1. 左值、将亡值、纯右值
C++11的值必定属于:左值、右值(将亡值、纯右值)三者之一。不是左值就是右值。详见值类别。
- 左值的特点:“有名字、可以取址”。没有名字或者不能取址,则必定是右值。
- 右值的特点:即将消亡,也就是说“会被析构”。
- 纯右值:一定没有名字。比如除去
string
之外字面值常量、函数返回值、运算表达式。
- 将亡值:即将消亡的值:比如临时变量,一旦离开作用域就会被销毁;可能没有名字,例如函数的返回值(非引用)。
示例:
1 2 3 4 5
| int main() { A(); getchar(); return 0; }
|
2. 引用、右值引用
右值引用涉及“右值”和“引用”两个概念。
- 引用不是对象,所以定义一个“右值引用”不会调用构造函数,避免了多余的构造过程。
- 右值是即将析构的值,把右值绑定到右值引用上,延长了右值的生命期,所以右值对象没有析构。
右值引用规则:
- 可以把左值绑定到左值引用。
- 可以把右值绑定到右值引用。
- 不允许把左值绑定到右值引用。
- 不允许把右值绑定到左值引用。
const
左值引用可以接受左值或右值。
示例:
1 2 3
| int a1 = 10; const int& aa = 10; int&& aaa = 10;
|
3. 引用和右值引用是左值
引用(包括右值引用)本身是左值,可以取址,但不能对右值取址。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <utility> using namespace std;
void fun1(int& t) { }
void fun2(int&& t) { }
int main() { int a = 10; int&& ra = move(a);
fun1(ra); fun2(move(a));
return 0; }
|
4. 复制构造函数和移动构造函数
为什么右值引用的构造函数被视为“移动”语义?
因为输入参数是一个引用(右值引用也是引用),可以直接访问所引对象的资源并接管它,同时将源对象的资源置空。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <iostream> using namespace std;
class A { public: A() { cout << "A()" << endl; } A(const A&) { cout << "A(const A&)" << endl; } A(A&&) { cout << "A(A&&)" << endl; } };
int main() { A a; A b(std::move(a)); return 0; }
|
5. 完美转发
完美转发是指对模板参数实现完美转发:即输入什么类型(左值、右值)的参数,就是什么类型的参数。
引用折叠规则:
- 如果有左值引用,优先折叠成左值引用。
- 如果只有右值引用,参数推导成右值引用。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <iostream> using namespace std;
void RunCode(int& m) { cout << "lvalue ref" << endl; } void RunCode(int&& m) { cout << "rvalue ref" << endl; }
template<typename T> void PerfectForward(T&& t) { RunCode(static_cast<T&&>(t)); }
int main() { int a = 10; PerfectForward(a); PerfectForward(move(a)); return 0; }
|
6. 移动构造函数、移动赋值函数、复制构造函数、复制赋值函数
移动构造函数的注意事项:
- 移动构造函数不允许抛出异常,建议添加
noexcept
关键字。
- 使用
std::move_if_noexcept
可以在移动构造函数抛出异常时回退到复制构造函数。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <iostream> using namespace std;
class A { public: A() { cout << "A()" << endl; } A(const A&) { cout << "A(const A&)" << endl; } A(A&&) noexcept { cout << "A(A&&)" << endl; } };
int main() { A a; A b(std::move(a)); return 0; }
|
7. 编译器优化
编译器默认会采用“返回值优化”(RVO或NRVO)。要观察移动语义与复制语义的不同,应该关闭编译器优化。
关闭优化命令:
1
| g++ -o test main.cc -fno-elide-constructors
|
8. 合成的移动操作
如果没有定义复制构造/赋值函数,编译器会为我们合成(浅复制)。但如果自定义了复制构造函数、复制赋值运算符或析构函数,编译器将不会合成移动构造/赋值函数。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <iostream> using namespace std;
class A { public: A() { cout << "A()" << endl; } A(const A&) { cout << "A(const A&)" << endl; } A(A&&) { cout << "A(A&&)" << endl; } };
int main() { A a; A b(std::move(a)); return 0; }
|
9. std::move
的实现
std::move
是一个类型转换,没有完成其他工作。
实现:
1 2 3 4
| template<class T> constexpr typename std::remove_reference<T>::type&& move(T&& t) noexcept { return static_cast<typename std::remove_reference<T>::type&&>(t); }
|
10. unique_ptr
与std::move
示例:
1 2 3 4 5 6 7 8 9
| #include <iostream> #include <memory> using namespace std;
int main() { unique_ptr<int> up(new int(10)); unique_ptr<int> p = std::move(up); return 0; }
|
解释:std::move
调用unique_ptr
的移动构造函数,转移up
所拥有的资源,并将up
置为空。
11. 右值与sizeof
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct A { int x, y; };
int f() { return 1; }
int main() { cout << sizeof(int()) << endl; cout << sizeof(10) << endl; cout << sizeof(A) << endl; cout << sizeof(A()) << endl; cout << sizeof(f()) << endl; }
|
参考资料
- 《深入理解C++11: C++新特性解析与应用》
- 《C++ Primer 第五版》
- C++中文 - API参考文档