✏️

Left / Right Value Reference

Tags

左右值

左值:可寻址的存储单元,可以由用户进行修改,比如变量。生命周期为作用域内
右值:反之,表示即将销毁的临时对象
 

纯右值和将亡值

C++98: 右值即为纯右值,如非引用返回的临时变量( int func(void) )、运算表达式产生的临时变量(b+c)、原始字面量(2)、lambda表达式等
C++11: 与右值引用相关的表达式,比如将要被「移动」的对象,T&&返回值、转换为T&&类型的转换函数的返回值
 
 

左值引用和右值引用

分别用T&和T&&引用到对应的值
左值引用可分为
  • 常量左值引用:可引用任意值,但是在之后的生命周期中只读
  • 非常量左值引用:只能引用左值
 
右值引用是对一个不具名的右值的别名,因为右值基本就是生命周期即将结束的值所以可以随意使用,右值引用本身是左值
无论左右值引用都需要立即初始化
 
 

移动构造函数和拷贝构造函数

拷贝构造函数:第一个参数是自身类型的引用,且任何额外参数都有默认值
由于经常被隐式调用,通常不会设置为explicit
合成的拷贝构造函数一般是将其参数的成员逐个拷贝到正在创建的对象中:类类型则递归调用拷贝构造函数,数组类型就依次拷贝
拷贝初始化发生时机:
  • =赋值
  • 将对象作为实参传递给非引用类型形参
  • 函数返回一个非引用类型的对象
  • 花括号列表初始化一个聚合类或数组
 
自定义类型没有显式声明这俩函数的时候,一般会生成默认的,except:
如果显式生成了拷贝构造函数,拷贝赋值运算符或者析构函数 or 类存在非 static 数据成员不能移动时,编译器不会主动合成移动构造函数和移动赋值运算符
如果显式声明了移动操作,则编译器删除默认拷贝操作
总结:仅显式声明其中一种则另一种编译器不会默认生成
根据三五原则,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作(析构+拷贝+移动)
 
 
移动构造函数需要完成以下任务:
  1. 接收一个右值引用参数
  1. 将源对象(参数里那个)中的指针赋值给新对象
  1. 将源对象中的指针置为nullptr(确切一点说,保证移动后源对象必须可析构),以防止源对象在析构时释放已被转移的资源
class Data { public: // 移动构造函数 Data(Data&& other) noexcept : ptr(other.ptr) { // 转移资源 other.ptr = nullptr; // 原对象置空 } // 拷贝构造函数 Data(const Data& other) { ptr = new int(*other.ptr); // 深拷贝 } private: int* ptr; }; Data a; Data b(std::move(a)); // 调用移动构造函数 Data c(a); // 调用拷贝构造函数
 
注:对于不抛出异常的移动操作,标记为noexcept,原因如下:
STL库容器对异常发生时其自身的行为提供保障
例如,vector 保证,如果我们调用 push_back 时发生异常,vector 自身不会发生改变
如果在移动过程中抛出了异常,那新对象没构造好,旧对象以及被修改,此时无法满足上述约束
为避免这种情况,vector会在移动构造函数被标记为noexcept时才会在reallocate的时候调用移动构造函数,否则调用拷贝构造函数
 
 

移动语义与std::move()

利用右值引用,避免不必要的深拷贝
std::move由于将左值强制转换为右值,触发移动语义
 
 

引用折叠

使用类型别名和模板参数可以产生“引用的引用”这种存在
typedef int&& IntRef; // using IntRef = int&&; template <typename T> void f3(T&&); int i = 42; f3(i); // valid,相当于把T看作int&,参数变成了T& &(引用的引用,根据下面的折叠规则折叠为一个普通左值引用T&) IntRef&& rrx = 10; // 折叠成右值引用
notion image
接受右值引用参数的模板函数:
template <typename T> void f3 (T&& val) { T t = val; // 拷贝还是绑定一个引用? t = fcn (t); // 赋值只改变t还是既改变又改变 val? if (val == t) { /* ... */ } // 若是引用类型,则一直为true }
如果val传的是42:T为int,完全当值来看
如果传的是一个左值,则T为int&,后略
 

std::move的原理

remove_reference见
✏️
Generic Progamming (Template)
类型转换标准库一节
// remove_reference<T>是type_traits里的一个工具,即T除去引用后的类型 template <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type&&>(t); }
由于有了引用折叠规则,t实则可以接受任意参数,并一定返回一个右值引用
注意move后t的值就不确定了
 

转发

某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们 需要保持被转发实参的所有性质,包括const-ness以及实参是左值还是右值
假如不使用转发,则一个右值作为实参时,其函数形参会将其变成左值
e.g.
// flip1 是一个不完整的实现: 顶层 const 和引用丟失了 template <typename F, typename T1, typename T2> void flipl(F f, T1 t1, T2 t2) { f(t2, t1); } // 假如f是: void f (int v1, int &v2) {// 注意 v2 是一个引用 cout <<< v1 << " " << ++v2 << endl; } // 下面这种情况下,i不会被修改,因为T1被实例化为int,传递给v2的是拷贝于i的副本t1 int i = 0; flip(f, i, 42); // 如果把flip1的参数定义为右值引用则可以解决上述问题: // T1此时会被推导为int&, T1&& -> int& && -> int&,T2正常推导为int,即 /* void flip2(void(*f)(int&&, int&), int& t1, int&& t2) { f(t2, t1); }*/ template <typename F, typename T1, typename T2> void flip2 (F f, T1 &&t1, T2 &&t2) { f(t2, t1); } // 新的问题:如果f的参数也是右值引用,那当把t2这个左值表达式传递给f的右值引用参数时一定报错 void g(int &&i, int& j) { cout << i << " " << j << endl; } flip2 (g, i, 42); // 错误:不能从一个左值实例化 int &&,即下面的错误
notion image
 
解决方案:std::forward<T>
将参数转换为T&&类型,存在引用折叠
template <typename F, typename T1, typename T2> void flip2 (F f, T1 &&t1, T2 &&t2) { f(std::forward<T2>(t2), std::forward<T1>(t1)); } // 此时调用flip2 (g, i, 42); 就可以了,例如T1 = int&, std::forward<T1> = int& && = int &