移动构造函数
首先对c++11中的多个概念做个定义
表达式expression 定义 由运算符op和运算对象类似于数学上的算数表达式。举例 字面值(literal)和变量(variable)时最简单的表达式,函数的返回值也被认为是表达式。
表达式是可求值的,对表达式求值将得到结果,结果拥有两个属性 类型 和 值类别。类型就是int,float…这些plain old type以及一些自定义的类类型
下面重点讲下值类别
在c++11以后,表达式按值类别分,必然属于以下三者之一:左值(left value,lvalue),将亡值(expiring value,xvalue),纯右值(pure rvalue,pralue)。其中,左值和将亡值合称泛左值(generalized lvalue,glvalue),纯右值和将亡值合称右值(right value,rvalue)
下面用 左值、右值.. 来指代 结果值类别为左值的表达式和结果值类别为右值的表达式。
下面对各个值类别的特征进行描述
左值表达式 lvalue 特点具名 是一个具体的对象 不是临时对象
能够用&取地址的表达式是左值表达式
举例 函数名和变量名(实际上是函数指针和具变量名—namespace::变量),模板型参对象,返回左值引用的函数调用、前置自增自减符连接的表达式++x/—x,由赋值运算符或符合赋值运算符连接的表达式(a=b a+=b a%=b),解引用表达式p,字符串字面值”abc”等。
纯右值表达式 prvalue 特点不具名 是匿名的 临时的
满足以下条件之一 1)本身就是纯粹的字面值如 3、false 2)求值结果相当于字面值或一个临时对象
举例 除字符串字面值以外的字面值、返回非引用类型的函数调用、后置自增自减运算符连接的表达式x++/x—、算术表达式 a+b,a&b,a<=b,a根据上面的说明要强调几点:
1)前缀自增自减是左值表达式 后缀自增自减是右值表达式—因为++i是对i加1后再赋值给i 返回的就是+1后的i,所以++i的结果是具名的 名字就是i。对于i++ 是对i先拷贝将得到的副本作为结果返回然后再对i加1,由于i++的返回结果是对i加1前的一份拷贝所以它是不具名的 所以无法作为左值被赋值。
2)解引用p是左值表达式 取地址&a是纯右值
我们在左值判断的说明中 说能被取地址&的表达式就是左值,因为&(p)正确所以p是左值表达式。对于&a而言得到的是a的地址 相当于unsinged int的字面值,所以是纯右值
3) a+b、a&&b、a== b
a+b得到的是不具名的临时对象,而a&&b a==b得到的非true即false相当于字面值
将亡值表达式 xvalue
c++11之前的右值和c++11的纯右值是等价的。所谓将亡值就是下列表达式
1) 返回右值引用的函数的 调用表达式
2) 转换为右值引用的转换函数的 调用表达式
返回临时对象引用的表达式—返回不具名的右值引用
右值引用 指可以绑定到右值上的引用,用&&来表示 如int &&rra=6(字面值是右值)
在c++11中用左值 去初始化一个对象(拷贝构造) 或 对一个已存在对象进行赋值(op = ),会调用op=或拷贝构造 来拷贝资源(堆上内存指针 锁等一些全局资源)。
c++11中用右值(包括纯右值—临时对象 or 字面值和将亡值—临时对象 or 字面值的引用)来初始化对象或赋值时,会调用移动构造函数(参数为引用的引用的拷贝构造) 或 移动运算符std::move来移动资源 避免拷贝提高效率。 当右值完成初始化对象或赋值时,右值的资源已经被移动给了被初始化者或被赋值者(),同时右值马上被析构—也就是说当一个右值准备进行初始化或赋值任务时 它已经将亡了,者也是将亡值名字的由来。
std::move(),tsatic_cast
注意
1 不是所有字面值都是纯右值,字符串字面值是唯一例外,字符串字面值是左值
&("abc");//--被编译器翻译为const char (*) [4]数组指针 (4是因为加上了\0)
char *p_char="abc";//p_char指向字符串首字符a的地址 这句话在c++14以后的版本会直接报错
2 右值特点匿名不具名
void foo(X&& x)//x为具名 所以是左值
{
X anotherX = x;
//后面还可以访问x
}
大X是自定义类型,X类中有一个成员变量指针p指向了一块堆上内存。X&&是X类型的右值引用(和X* 是指向X类型的指针一个道理),
函数内X anotherX=x; 会调用op= 因为x是右值引用类型 所以会调用op=(X&&) 移动赋值函数。
移动构造函数的需要做的工作就是将x的堆上内存指针赋值给anotherX的p成员,然后将x的p指针置为nullptr。 在赋值完成以后,我们再去查看x.p就已经变为nullptr了,如果我们不小心再使用x将会造成意象不到的错误。
引用类型的形参以及返回值是不需要构造的因为引用的本质是指针,纯右值和右值引用&&都能够触发移动构造函数
构造具体对象形参x时 f(X x),传入X()这个临时对象纯右值 f(X())会调用移动构造来构造x,传入右值引用 比如X&& foo()这个函数返回的是一个右值引用 则f(foo()) 传入的是右值引用 也会调用移动构造
移动构造函数
上图描述的是正确的拷贝构造函数和移动构造函数应该执行的行为,对于有堆内存对象—-在将临时对象传入形参时发生的拷贝构造行为, 为形参对象专门分配一块堆内存,并将临时对象堆内存上的内存拷贝到临时对象的堆内存里,并且在构造完毕后 临时对象自动析构,并在析构中释放其拥有的堆上内存。
—-在将临时对象传入形参时发生的移动构造行为,将形参对象中的指针成员指向临时对象的堆上内存,并且在构造完毕后 临时对象自动析构,但析构的时候不释放堆上内存,因为当前还有形参对象指向这块内存 还要使用这块内存。(形参对象偷了临时变量的内存空间,据为己用。节省了开辟空间的时间。)
要尽量保证移动构造函数 不发生异常,可以通过noexcept关键字,这可以保证移动构造函数中抛出来的异常会直接调用terminate终止程序。
右值引用就是对一个右值进行引用的类型,右值通常不具有名字,我们就只能通过引用的方式找到它的存在了,右值引用就是让返回的右值(临时对象)重获新生,延长生命周期。临时对象析构了,但是右值引用存活。
右值临时对象可以用于初始化右值引用&&,这种情况下该右值所标识的对象内的全局资源生存期被延长到该引用的作用域结尾。
不过要注意的是,右值引用不能绑定左值:int a; int &&c = a; 这样是不行的。
int&& c=1; ok因为1是字面值纯右值
//////////////////////////////////////////////////////
当我们想要测试这个移动构造函数时,通常会想创造一个临时对象去构造一个有名对象,就会写成下面这样
X another_X(X());//本意 用临时对象X()去调用移动构造函数去构造another_X这个对象,但是并没有调用移动构造!
这是个很经典的c++ Most vexing parse” 问题。
我们来分析以下编译器看到X another_X(X())时它会怎么翻译
它会认为X another_X(X());这是一个函数声明,another_X是函数名,X是返回类型, 参数名 无 参数类型 函数指针,函数指针的返回类型为X 函数无参数。
void f() {cout << 1;}
using X= void;
X another_X(X());//函数声明 X()被当作了函数指针X another_X(X p()) {p();}//函数定义
int main() {another_X(f);}//函数调用
参见如上代码理解,编译器结果输出1
其实不止X another_X(X());被误解为函数声明,X(X())本意用一个临时对象去构造另一个临时对象也会被解析为函数声明。
为了避免歧义c++11中,X another_X{ X{} };这样会调用移动构造函数,通过大括号{}来避免歧义。或者X another_X{ X() };或者X another_X( ( X() ) ); 为X()再加一个()也能让编译器理解你的意图。
其实要得到右值引用 最靠谱的是用std::move 这样一定会去调用移动构造函数X another_X(std::move( X() ) );
////////////////////////////////////////////////////////////
在容器中已经有一个元素的情况下,再push_back一个对象,容器会重新申请内存vector growup,growed up会引起内存的搬移(在内存管理中有提到),内存搬移(内容拷贝)的时候如果我们类对象的移动构造函数是nonexcept的会调用移动构造。并且注意要让容器知道X类型的移动构造函数不会抛出异常(给移动构造函数加nonexcept)容器才会在重新分配内存的情况下调用移动构造函数,否则都是强制调用拷贝构造函数。
vector为例,如果调用push_back或者emplace_back时,由于容量不够,而触发动态扩容的时候,会将原来存储的对象全部复制或者移动(如果对象有nonexcept移动构造函数)到扩容后的空间中去,此之谓”reallocation”
注意一般情况下像我们上面一样显示调用移动构造函数都会失败,因为编译器会自动做优化,这种情况下想要调用移动赋值 需要将编译器的Copy elision选项关闭用g++编译的时候加上-fno-elide-constructors可以禁止掉g++的这个优化
Copy elision 省略不必要的拷贝
技术至少包括以下两项内容:
1 返回值优化(RVO return value optimization),即通过将返回值所占空间的分配地点从被调用端转移至调用端的手段来避免拷贝操作。
返回值优化包括具名返回值优化(NRVO named return value optimization)与无名返回值优化(URVO unnamed return value optimization),两者的区别在于返回值是具名的局部变量还是无名的临时对象。
2 右值拷贝优化,当某一个类类型的临时对象被拷贝赋予同一类型的另一个对象时,通过直接利用该临时对象的方法来避免拷贝操作。
这项优化只能用于右值(临时对象),不能用于左值。
例子如下面这个例子 多次用临时对象去构造临时对象 x的cv类型和右边的cv类型相同
T x = T(T(T())); // 只会调用一次T的默认构造函数
例子如下面这个例子 函数返回临时对象 且类型和函数返回值类型相同
T f() { return T{}; }// 当返回时还需要创建一个临时对象作为返回值
// 所以就需要用返回值T{}去构造这个临时对象 T(T{}) 就是上面讲的问题
// 多次用临时对象去构造临时对象 编译器会将其优化为只构造一次
T x = f(); // 只会调用一次T的默认构造函数
T* p = new T(f()); // 只会调用一次T的默认构造函数
当 copy elision 发生时,拷贝/ 移动(since C++11) 操作的省略,是通过将目标及源对象视为同一对象的两个不同引用实现的,它的析构并不会被省略,只会推迟至两个对象均已销毁时发生 (除非目标对象是通过其右值引用版本的构造函数创建的,这种情况下,析构将发生在目标对象销毁时)
坑1
具体的暂时不讲了,以后有空好好看下https://zh.cppreference.com//w/cpp/language/copy_elision
尽管C++11以上标准提出“复制省略(copy elision)”优化策略,并且GCC等主流编译器均支持该优化,但我强烈不建议使用该技术。这样写出来的代码还有C++的味道吗?如果C++可以直接返回类对象,那还要C++干什么?直接使用Python或者Matlab就行了。我们在使用C++语言时,必须牢记,返回值必须是int、bool、枚举或指针等轻量级原生(lightweight primitive)数据类型。如果确实需要返回大型数据,请使用引用或指针作为输出参数返回,而不是通过return语句返回。