0%

C++:移动语义和右值引用

移动语义与右值引用

1.值类别

1)左值

  • 具名,可取地址
  • 非常 non-const左值可以放在赋值运算符的左侧
  • 常见情况
    • 变量
    • 左值对象的成员
    • 返回左值引用的表达式,如 ++xx = 1
    • 字符串字面量,如 "abc"
      • 字符串字面值为左值一个最重要的原因是可以获取其地址,cout << &"abc" <<endl; 可以正常编译并运行。这是因为C++将字符串左值实现为 char型数组,为其分配了空间并且允许程序员对其进行操作

2)纯右值

  • 不具名、不能取地址的“临时对象”
  • 不可以放在赋值运算符的左侧
  • 常见情况
    • 返回类型非引用的函数调用或运算符表达式,如 x++1 + 2
    • 除字符串字面量外的字面量,如 true、 42
    • lambda表达式

3)将亡值

  • C++11引入,和纯右值合称为“右值”
  • 不可以放在赋值运算符的左侧
  • 常见情况
    • 右值对象或者数组的成员
    • 返回右值引用的表达式,如 move(x)——转换为右值引用 若x是int,则move(x)是int &&

2.重要的成员函数重载

1
2
3
4
5
6
7
// 拷贝,当传入参数为左值时调用拷贝构造
T::T(const T& rhs);
T& T::operator=(const T& rhs);

// 移动,当传入参数为右值时调用移动构造
T::T(T&& rhs);
T& T::operator=(T&& rhs);

3.移动的意义

  • 允许资源的传递
  • 允许返回大对象和容器
    • 一般同时使用异常来表示错误

4.移动和 noexcept

noexcept表示函数不会抛出异常

下列成员函数一般不允许抛出异常

  • 析构函数
  • 移动构造函数,如果移动构造没有标成 noexcept,比如说 vector在动态调整大小的时候都不会调用移动构造在移动元素
  • 移动赋值运算符
  • 交换函数(swap

5.五法则

因为用户定义析构函数拷贝构造函数拷贝赋值运算符的存在阻止移动构造函数移动赋值运算符的隐式定义,所以任何想要移动语义的类应当声明全部五个特殊成员函数

6.右值引用的误用

1
2
3
4
5
6
7
8
9
10
Obj&& wrong_move(){
Obj obj;
return move(obj);// 未定义的行为
}// 返回栈上对象的指针或者引用永远都是错的,不管是左值还是右值

Obj bad_move(){
Obj obj;
return move(obj);
}
// 如果要返回函数内部即栈上的对象,直接return obj就行

7.坍缩规则和转发引用

Q:Vector(Vector&& rhs)这里的 rhs是左值还是右值?

A:右值引用变量有标识符,所以是左值

——所以使用右值引用调用其他函数需要加上 move()保持右值属性

Q:在通用的函数模板里怎么办?

A:分别写两个不同的重载

1
2
3
4
5
6
7
8
9
template<typename T>
void bar(T &s){
foo(s);
}

template<typename T>
void bar(T &&s){
foo(move(s));
}

问题:重复、啰嗦

引入转发引用

  1. 坍缩规则

    T& & → T&T& && → T&T&& & → T&T&& && → T&&

    所以只有 T&& &&会转化为右值引用

  2. 所以 T&&不是右值引用,当其出现在函数模板的参数或者变量声明中时 T&&转发引用

  3. std::move(x):把x转换成右值引用

  4. std::forward<T>(x):保持x的引用类型——传进来的T是左值,进函数的也是左值,右值也一样

1
2
3
4
5
6
7
8
9
10
11
// forward使用示例
template<typename T>
void bar(T&& s){
foo(std:forward<T>(s));
}// 不用写两个重载

int main(){
circle temp;
bar(temp);
bar(circle());
}

8.临时对象的生命周期

1
2
3
4
5
class result;
result process_shape(const shape&,const shape&);

process_shape(circle(),triangle());
// circle triangle result 对象在这条语句执行完成后销毁

**生命期延长规则 **

  • 如果一个 prvalue(纯右值) 被绑定到一个引用上,它的生命周期会延长到跟这个引用变量一样长

    result&& r=process_shape(circle(),triangle());

    则右边这个临时对象的生命周期会延长到 r 离开作用域

  • 如果是 xvalue(将亡值) ,则不能延长

    result&& r=move(process_shape(circle(),triangle())); 这种写法是错误的

C++对象的自动生命周期

  1. 后创建的先析构
  2. 全局对象和静态对象在进入 main 之前创建
  3. 函数静态对象在第一次执行到声明语句时创建
  4. 函数自动对象在定义时创建,到定义的所在的 } 即析构
  5. 临时对象在当前语句执行完成后即析构(除非赋值给引用变量而延长生命期)

经典习题:析构顺序

1
2
3
4
5
6
7
8
9
C c;
int main(){
A *pa=new A();
B b;
static D d;
delete pa;
}
c pa b d
~pa ~b ~d ~c