引用,值类别, 移动语义, rvo/nrvo, 程序优化

二. 说清楚值类别,需要从其演变历程说起。
1.在C语言中,把表达式的值类别分为 "左值"与 "非左值" 与 "既不属于左值也不属于非左值" 三种类别,不存在右值这个概念。
1.1)C语言中的左值有哪些?
1.1.1)具名表达式(比如变量名,数组名,但是不包括函数名)
1.1.2)数组类型的表达式

int arr[3][4];
int(*p)[4] = arr;

arr   ->类型是 int[3][4]
*arr ->类型是 int[4]
*p ->  类型是int[4]

形如这三种,都属于数组类型的表达式,都属于左值

1.1.3)字符串形式的字面值
1.1.4)一切返回左值的运算符表达式,比如*运算符表达式,[]运算符表达式,等等略了。
1.2)C语言中的非左值有哪些?

1.2.1)非字符串形式的字面值(整数字面值,字符字面值,浮点数字面值)

1.2.2)  所有不返回左值的运算符表达式,
包括函数调用表达式,
值转换方式的类型转换表达式(不包括转换为数组类型的类型转换表达式),
算术/关系/逻辑/位运算符表达式,
自增自减运算符表达式,赋值/复合赋值运算符表达式,条件运算符表达式,逗号运算符表达式等等。
取地址运算符表达式。
1.3)C语言中的既不属于左值也不属于非左值的表达式有哪些?
函数类型的表达式,比如函数名,或者函数指针的解引用表达式。
虽然函数类型既不是左值也不是非左值,但是函数类型在大多数情况下会转换退化成值类别为非左值的函数指针,就像数组类型会转换退化成指针一样。

2. C++98中,把C语言中的非左值称为了右值,且令函数类型也为左值。因为同时引入了"引用"的概念,此时才真正明确了左值与右值的概念定义。

2.1)何为左值?
loactor value,表示可以定位其内存的值,即左值要求不仅是占内存,更侧重可以使用&运算符直接取其地址。比如说因为临时对象虽然占内存,但是无法进一步的使用&运算符取地址,所以临时对象不是左值,而是右值。
2.2)何为右值?
read value,不能使用&运算符直接进行取地址操作的都是右值。
但是对于右值不能取地址操作的原因分为两种,一是因本身不占内存,所以无法对其取地址,比如常量表达式。二是本身是占内存的,但是无法直接对其取地址,比如临时对象。
2.3)哪些属于左值,哪些属于右值?
2.3.1) 左值分类:
--字符串形式的字面值,需要注意的是,字符串形式字面量在类中会是是右值,当其位于普通函数中才是左值。
--标识内存空间的名字都是左值,比如(变量名,形参名,对象名,数组名,函数名,指针名,类成员指针,等等)
--具名的左值引用, 包括const引用。
--无名的左值引用,包括const引用.   即(Type&)expression
--返回左值的一些C++内置运算符表达式。
所以说确定一个表达式是左值还是右值与其是否可以被修改的因素无关。比如说当试图去引用一个不可修改的左值时,一般必须使用const引用,话说回来使用const引用的原因并不是因为其是右值。只能说const引用既可以用来引用右值,也可以用来引用不可修改的左值。

2.3.2)右值分类
--除了字符串字面量以外的字面量,比如42,布尔常量(true,false),nullptr都是右值
--临时对象
  这里说一下何为临时对象

//1.函数值传递时产生临时对象
这种临时对象是如何产生的:在函数内部形参处,产生形参临时对象(空间),然后这个形参对象会调用拷贝构造函数,使用实参对象对该形参对象进行初始化,形参属于具名的"临时"的局部对象,即称之为伪临时对象(扯蛋的,拿来充数)
.//2.函数(包括运算符重载函数或普通函数)值返回时产生的临时对象
1)这种临时对象如何产生的:在函数外部,即在函数调用处先产生一个临时对象(空间),然后这个临时对象会调用拷贝构造函数使用函数内部返回的对象对该临时对象进行初始化。
简而言之,通过函数返回的对象去在函数调用处拷贝构造生成一个临时对象.
函数外部调用返回处产生的是无名的临时对象,即真实存在但是肉眼看不到的真正临时对象
2)拷贝构造函数和临时对象产生的先后关系:
错误理解:调用拷贝构造函数生成临时对象。
正确理解:先生成一个未初始化的临时空间,有了这个临时空间编译器再调用拷贝构造函数在其中构造初始化一个临时对象出来。
3)为什么不能用返回引用代替值返回来避免临时对象的产生呢?
很多时候函数都是需要返回一个栈对象的值。但是为了返回这个值,不能返回栈对象的引用,因栈对象会消亡,只能返回栈对象的值。
所以为了返回一个值到外面,那外面必须得有一个临时空间来承接这个值,这也是值返回需要产生临时对象的原因。
非优化的常规情况下,无论外面用何种方式接这个值,这个临时对象的产生都不可避免。即以值方式返回后,在外面接返回的代码处,基本优化不了什么,不管是用引用接,还是用对象接,临时对象的产生不可避免。
//3.转换目标非引用类型的类型转换时产生的临时对象
1)这种临时对象如何产生的:产生一个目标类型的临时对象(空间),调用构造函数(或内建工具)使用源类型的对象的值对其进行转换构造初始化。
产生的是无名的临时对象,属于真正的临时对象

//4.1使用const引用去引用一个右值或类型不匹配的对象时产生的临时对象
//4.2转换到const引用的类型转换表达式,有时会促使生成临时对象
//5.手动创建的匿名对象
//6.内置运算符表达式产生的临时对象。

 

--手动定义创建的匿名对象
--this指针
--枚举项
--一些返回右值的内建运算符表达式,比如转换目标是非引用类型的类型转换表达式,内置的&取地址表达式,后置++--表达式,等等。
2.3.3) 特别说明
需要特别说明的是数组类型的表达式和函数类型的表达式,因为它们存在二义性,需要从两方面来分析
2.3.3.1)数组类型的表达式,无论是有名(即数组名)还是无名的数组类型的表达式。当把其视为数组类型时,那么它就是左值,当把其视为指针类型(即首元素的地址)时,那么它就是右值.

int arr[3][4] = { 0 };
1)把表达式视为数组类型的左值    
int(&ra1)[3][4]= arr;//正确
int(&&rra1)[3][4] = arr;//错误

int(&ra3)[4] = *arr;//正确 
int(&&rra3)[4] = *arr;//错误


2)把表达式视为指针类型的右值
int(*const &ra2)[4]=arr; //正确 
int(* && ra2)[4] = arr;//正确
int(* &ra2)[4]=arr; //错误

int* const& ra4 = *arr;//正确
int* && rra4 = *arr;//正确
int* & ra4 = *arr;//错误

int(*const &ra5)[4] = &*arr;//正确
int(* && rra5)[4] = &*arr;//正确
int(*&ra5)[4] = &*arr;//错误


int(*pa)[4] = arr;
1)把表达式视为数组类型的左值
int(&rp1)[4] = *pa;//正确
int(&&rrp1)[4] = *pa; //错误
2)把表达式视为指针类型的右值
int * const &rp2= *pa;//正确
int* && rrp2 = *pa;//正确
int * &rp2= *pa;//错误

 

2.3.3.2)字符串常量本质是字符数组类型,所以也具备双重身份

string const&r = "abcd";
const char* const& r = "abcd";
const char(&r)[5] = "abcd";

2.3.3.3)对于函数类型的表达式,无论是有名(即函数名)还是无名的函数类型的表达式。
a)把函数名视为函数类型时即是左值,也是右值。
把函数名视为函数指针类型时即是左值,也是右值。

/函数名 与 *函数名 都具备二义性,同时具备左值与右值的身份。
//&函数名就是纯函数指针类型,是个右值

int func(int a)
{
    cout << "func" << endl;
    return a;
}

int( &r)(int)  = func;//正确   
int(&& rr)(int) = func;//正确
int(* const& r2)(int) = func;//正确
int(*&&rr2)(int) = func; //正确

int( &r)(int)  = *func;//正确   
int(&& rr)(int) = *func;//正确
int(* const& r2)(int) = *func;//正确
int(*&&rr2)(int) = *func; //正确

b)函数指针名,只能是函数指针类型,是左值。

*函数指针名,视为函数类型时,即是左值,也是右值。视为函数指针类型时,只能是左值(且是不可修改的左值),只能用const引用去进行引用。

int func(int a)
{
    cout << "func" << endl;
    return a;
}
int(*p)(int) = func;
cout << typeid(p).name() << endl;
cout << typeid(*p).name() << endl;
int(&p1)(int)= *p; //函数类型可以作为左值,函数类型
int(&&pp1)(int) = *p;//函数类型可以作为右值

int(* const& p2)(int) = *p; //解引用后得到的函数指针类型只能作为不可修改的左值
int(* && pp2)(int) = *p;//错误,因为是左值
int func(inta) { 成本<< “func” <

3. 到了C++11后,新标准对右值类别进行了再次划分,

3.1)纯右值pvalue: 

原先C++98中的所有右值在C++11中都改名为纯右值。另外新增了lambda表达式,也是纯右值。
3.2)将亡值xvalue: 
将亡值只有一种,即无名的右值引用,
可以通过多种方式获取到无名的右值引用表达式

比如通过转换到无名右值引用的类型转换表达式来获取,

比如通过返回类型是无名右值引用类型的函数调用表达式或者运算符重载函数调用表达式,

而std::move返回的就是无名右值引用 来充当转换生成的右值,std:forward也会根据情况返回
值得一提的是,无名右值引用中有个特殊的:如果是函数类型的无名右值引用既可以作左值也是作右值(将亡值)

3.3) 左值,右值(纯右值,将亡值),右值引用,移动语义,std::move的伦理关系
3.3.1)引入右值引用类型的目的
是为了支持移动操作,新标准引入了一种新的引用类型右值引用rvalue reference。
右值引用有一个重要的性质——它只能绑定到一个将要销毁的且无其它用户的对象。这两特性意味着,我们可以自由地将一个右值引用所引用的资源“移动”到另一个对象中。即拿着右值引用这个句柄去放心的进行资源移动操作。因为用户知道右值引用所引用的资源必是可安全移动的。所以引入右值引用的目的是做到让使用右值引用的代码(移动语义操作代码)可以自由地接管所引用的对象的资源,这些代码可以在安全的前提下窃取某个对象的资源转移到另一个对象中。


那为什么很确定的说右值引用所绑定的这种将要销毁的对象无其它用户?

右值引用不能连续进行以右值身份进行传递。因为右值引用本身是左值。
不存在多个右值引用句柄同时引用某一个即将销毁的右值对象。

A a,b;
A&& r1 = a + b;
A&& r2 = r1;//错误,r1本身是左值,
所以不存在多个右值引用去引用同一份资源。做到了一个右值引用所引用的对象只有当前唯一用户句柄。

//但是不要这样做。不要试图再将一个纯右值身份的右值引用刻意转化成将亡值身份的无名右值引用,不仅没意义还容易引发风险。
A&& r2 = (A&&)r1;//虽然语法没问题,但是会导致一份临时对象被多个右值引用一起共享了。在后面可能会进行的移动操作上带来大问题。编写语法分析代码虽好,但是不要违背C++语法的设计初衷。


下面这种,两次a+b不属于同一份资源。是独立不同的两个右值对象。不属于上面所说的同一份右值对象的范畴。
A a,b;
A&& r1 = a+b;//生成临时对象sum1
A&& r2 = a+b;//生成临时对象sum2

3.3.2)引入将亡值的目的:把左值转换为将亡值形式的右值

为了支持对左值也能进行移动操作,
引入了将亡值这种专属于无名右值引用类型的值类别,提供了直接可通过类型转化的方式把左值转换成右值。
即是为了能够把后续不再使用的左值也能够转化成无名右值引用这种将亡值概念形式的右值,成为右值后就也可以对其进行移动语义以此提升性能了。
当一个左值被转换成右值引用后,那么这个左值就拥有了一个新身份,即将亡值,此时它就是属于将被移动的对象。

但是不要试图把一个左值身份的具名右值引用再次转换成将亡值身份的无名右值引用,不然可能会写出让同一个即将销毁的对象有多个具名右值引用的用户句柄的代码,导致后续对该对象进行移动操作出现大问题。 就是说把一个已经绑定到某个对象上的具名右值引用句柄转换成无名右值引用,再次连续以右值身份被绑定到另一个具名左值引用上,这种连续操作会导致某个即将销毁的临时对象同时被多个右值引用句柄所共享了。导致后续拿着这些具名右值引用句柄对这个临时对象进行移动操作时就会出现问题。例子就是上面那个.

总而言之,引入将亡值的概念,是让左值也能进行移动语义,即去把不再使用的左值主动转成右值,也就是转换成匿名右值引用类型的右值,这种右值是一种新右值,为了在意义上和旧右值做个区分,则把旧右值称为纯右值,把新右值称呼为将亡值,统称为右值。


3.3.3)当然也可以把纯右值转化成将亡值,但没啥意义,纯右值本来就是右值,用纯右值进行初始化或赋值一个对象时本来就能进行移动语义,所以没必要脱裤子放屁。所以就目前阶段来说,只有把左值转化成将亡值有意义。

另外在把纯右值转化成将亡值去初始化一个对象这个场景时,可能并不只是脱裤子放屁,甚至可能会低效,尤其是在C++17后RVO被强制为语言级别的规则常态时。因为把纯右值转换为将亡值去作初始化器后,优先级更高的RVO会失效,毕竟RVO只作用于纯右值,

class A
{
public:
    int value;

    A(int v = 0)
    {
        cout << "A()" << endl;
    }
    A(const A& another)
    {
        cout << "A(const A& another)" << endl;
    }
    A( A&& another)
    {
        cout << "A( A&& another)" << endl;
    }

    A& operator=(const A& another)
    {

    }
    ~A()
    {
        cout << "~A()" << endl;
    }
};


int main()
{
    A a1 = A();//RVO,刚刚的好
    A a2 = std::move(A());//本次不RVO优化了,去作移动语义了。
    A a3 = static_cast<A&&>(A());//虽然不是纯右值了,但VS里仍然是优化的,但应该是copy elision优化。

    return 0;
}

 

 











一. 引用(左值引用)的简述
1.先说说引用的几个基本特点
1.1)引用因为在语法上是一种关系型说明,所以其不能独立存在,定义时必须初始化且与被引用的对象类型保持一致,仅论语法层面的话是不为其分配内存的,它只是原对象的别名作用,在后续的使用上与直接通过对象原标识名进行操作没有任何区别。
1.2)同样是因为是关系型说明,所以引用一经声明,不可改变原有的引用关系。

1.3)可以对一个引用再次进行引用,多次引用所带来的效果,也仅是让一个对象具有多个平级关系的别名,因此引入引用的目的是在数据传递的环节上一定程度的取代指针传递,摒弃了指针的多层级特点及麻烦用法,使之拥有了平级解决问题且简单易用的新特性,换句话说就是避免了传递N级指针去解决N-1级的问题,而是在平级内解决问题,所以说在函数之间传递引用时,引用从宏观上的作用就是扩展了变量的作用域,传参后就像在本地解决问题一样。

2.指针与引用的区别。
2.1)尽量不要拿引用与指针来对比学习,因为在语法层面,引用与指针是完全两码事,引用仅是原对象的别名而已,语法层面并不占内存,屏蔽了引用底层对内存的获取与存储处理操作(类似取地址和解引用访问空间等操作)。
2.2)在底层实现层面,首先要说的是引用与指针在脱去了语法外壳后在汇编代码层面是完全相同的。但是脱去了语法外壳还谈何语法,引用的底层并非存在一个指针类型的变量,并非是对指针的再次包装,而是说引用的实现方式是以类似实现指针的方式来实现的,所以在底层角度的实现上其同样必定需要存储空间来保存被引用对象的内存地址.

3. 转换为引用的意义
前面已经知道了引用的基本特点,这里再说说转换为引用的类型转换表达式所表示的无名引用,
目前这里只涉及到转换为非继承关系的类类型和内置类型方面的引用的话题,更多详情见C/C++类型转换专题。
此时需要进行类比来学习比较 (int)value 与 (int&)value 的工作原理:

3.1)首先要先了解两种类型转换,
  3.1.1)值类型转换:进行二进制的转换,即值层面的转换,是对提取出的值进行转换,此种转换方式会生成临时对象(或称变量),
说是以原类型的读取格式把值读取出来 再把这个值转换为新类型,并以新类型的存储方式来存储进看不到的临时空间里。
  3.1.2) 重解释转换:  仅仅是重新解释了给出的对象的比特模型,即只是以指定的(读取)格式来重解释一块内存空间,并没有进行二进制的转换即值转换,因此不会产生临时对象。
3.2)  (int)value,是进行值转换,会生成一份临时空间,如果强转目标类型位数大于原类型,那么会进行截断取原数据拷贝进临时空间中。
换一个角度说,(int)value实际上是以value为操作数新构造了一个整型数。

3.3) (int&)value, 是告诉编译器将value所占内存中的数据当作整数看,即重新对这块内存进行解释. 几乎等价于 *((int*)&value),并没有产生临时空间。就相当于用int型直接读出了标识符value所代表内存地址中的数据。

二. const引用(属于特殊的左值引用)
1. const引用的语法特点
1.1)const引用既能引用左值也能引用右值。

1.2)const引用中的“const”是底层const

1.3)const引用的几种基本作用

1.3.1)const引用用来延长临时对象的生存期

//延长给函数传入的临时对象实参
void func(const A & r)
{
  
}

func(A());

//延长各种情况下的临时对象的生存期
int a,b;
const int & r = a + b;
float f;
const int & r2 = f;
const int & r2 = (int)f;

//延长函数值返回所生成的临时对象的生存期
A func()
{
    A a;
    return a;
}
const A &r = func();


A func()
{
    
    return A();
}
const A& r = func();


A func(const A& t)
{
    return t;
}
 A a;
 const A& r = func(a);
举例延长生存期的场景
//1
class B
{
    public:
    B(int t)
    {   
        v = t;
        cout<<"B()"<<endl;
    }
    B(const B& another)
    {   
        v = another.v;
        cout<<"B(const B& another)"<<endl;
    }
    ~B()
    {   
        cout<<"~B()"<<endl;
    }
    int v;
};

class A
{
    public:
    A():r(B(5))//看看const&r能不能延长这个无名临时对象的生存期,看看临时对象的空间还会不会随构造函数的结束而销毁。(没延长,还是立马析构销毁了)
    {
        cout<<"A()"<<endl;
    }
    A(const A& another):r(another.r)
    {
        cout<<"A(const A& another)"<<endl;
    }
    ~A()
    {

        cout<<"~A()"<<endl;
    }
    const B& r;
};

//2
A& func()
{
    A a;
    return a;
}
const A &r = func();//没延长,已经随函数栈销毁而被释放了、
举例无法延长生存期的场景

1.3.2) const引用促使编译器生成临时对象
1.3.2.1)在一些情况下,使用const引用/右值引用,会促使系统产生临时变量,系统不会为左值引用产生临时变量的。
现在下面列举的这几个场景使用const引用的目的都是为了促使系统生成临时对象的。几乎都是一回事。

a.)

const int& r = 10;//因为是const引用,所以会促使系统分配临时空间,生成临时对象。

b.)

double d = 3.14;
const int &rd = d;//因为const引用,所以会促使发生隐式类型转换生成临时对象,再让const引用去引用这个临时对象。

 

c.)

//A是类类型,已经实现了int->A的类型转换

const A& r = 10;//因为是const引用,所以系统会分配临时空间,调用构造,转换生成临时A对象。

//AB都是类类型,前提是已经实现了B->A的类型转换
B b;
const A& r = b;//因为是const引用,所以系统会分配临时空间,调用构造,转换生成临时A对象。

 

d.) 如果实参与引用形参的类型不匹配,如果使用形参是普通引用就不会产生临时变量。那么就会报错。此时就需要用const来修饰引用形参来接实参,因为使用const引用可以使系统能够正确的生成并使用临时变量。

#include <iostream> 
using namespace std; 
 
int add1(int &a, int & b) 
{ 
        return a + b; 
} 
 
int add2(const int &a, const int & b) 
{ 
        return a + b; 
} 
 
int main() 
{  
        cout<<add1(3,5)<<endl;//错误 
        cout<<add2(3,5)<<endl;//正确
 
        return 0; 
} 

1.3.2.2)以上的情况,并不是因为所引用的是常量才用的const引用或右值引用。而是因为只有使用了const引用或右值引用才能让系统为其生成类型匹配的临时对象,有了真实地址供其引用。所以如果不用const引用就报错的原因是由于没有生成临时对象,没有类型匹配的真实内存地址供普通左值引用绑定,才会报错。

而下面这些情况下的临时对象并不是const引用促使系统生成的,而是本身自己就主动生成的,使用了const引用的目的是因为其是右值

对表达式的值:
int a = 3;
int b = 5;
int &ret = a+b;//
const int &ret = a+b;


对函数的返回:使用const引用来引用函数值返回时所产生的临时变量

现有一个函数 int foo();
int fa = foo();
int &ra = foo();//
const int &ra = foo();

 

1.3.2.3)还有一些情况下使用const引用的原因是因为所引用的对象本身就显示标明了是具有const属性的左值

1.3.3)把函数形参设为const引用 可以提高传递实参的灵活性,那么形参就可以接右值形式的实参或者const实参了。与1.3.2原理一致,只是换了个角度说的。

1.4) 当const引用去引用指针时时所存在的一个问题
很麻烦,待补
1.5) 用一个引用去引用数组类型或函数类型时,该引用不能是常引用。

三. 右值引用

在C++11新标准中引入了右值引用类型。
带来了一个新的左值:具名右值引用。
带来了一个新的右值:无名右值引用。
虽然说具名右值引用是左值,无名右值引用是右值,但是涉及到函数类型的右值引用有点特殊。在值类别专题说。
1. 右值引用相关特点
1.1) 右值引用只能引用右值。
1.2) 具名的右值引用能延长临时对象的生存期,相当于让临时对象具名化。起了个名字。与具名的const引用同理,但不能通过const引用修改临时对象。
1.3) 具名右值引用本身是左值,可以调用非const成员函数也可以调用const成员函数。而const引用只能调用const成员函数
1.4) 标准规定右值引用参数的绑定匹配优先度要高于const引用参数。换句话说就是标准规定实参匹配到右值引用形参的优先度要高于匹配到const引用参数。
2. [定义右值引用]   [返回右值引用类型]  [转换为一个右值引用]  三种操作意义有何区别

//1.
A&& foo()
{
    A a;
    return a;
}
//这种情况,相当于是定义了一个无名右值引用,再把这个右值引用去引用A类型对象,类似于定义语句。但是因为a是左值,所以编译错误
//所以这样才对
A&& foo()
{
    return A();
}


//2.
//而这种情况是把A类型对象直接转换成一个A类型的右值引用的类型。编译正确。与(A&)a不同的只是值类别不同。
A a;
(A&&)a;


//3 没什么说的
A a;
A&& r   =  (A)a;

3. 右值引用的作用:

3.1)引入右值引用的目的1,为了能够替代const引用去引用右值,既延长了临时对象的生存期,同时也可以做到修改临时对象的目的,取到临时对象地址的目的

const引用可以接临时变量,让函数调用处的生成的临时变量不用再拷贝给形参了,再没这个拷贝操作了。引用可以直接引用到临时变量的地址。而指针是做不到的,指针是是无法获取接收到临时变量的地址的。
void func(int a)
{
}
func(5);用5来生成int类型临时变量,再拷贝给形参,拷贝动作是多余的,而生成临时变量不可避免不可少。

引用是为类对象而生的原因之一,因为对象一般很大,无法使用寄存器来存放临时变量,
对于内置的数据类型,那么引用完全就是在添乱,因为首先对于常量,根本不需要占用内存(栈内存)和寄存器,
是可以写在汇编代码中的,占用的是代码段的空间,还有两外两种情况,因为临时变量就是int,那么我完全可以用寄存器来存放,速度更快,因为引用使用的是内存地址,这样会增加一次内存访问,这就是为什么引用是为类对象而生的原因之一,因为对象一般很大,无法使用寄存器来存放临时变量 但是const引用又限制了使用

右值引用就可以引用临时变量,并对其进行修改

3.2)更重要的是基于右值引用所支撑扩展的的两种C++新机制:移动语义和完美转发

posted on 2024-09-29 17:38  有点头皮发麻  阅读(86)  评论(0)    收藏  举报