Effective Modern C++ 笔记 - 右值引用, Move语义和完美转发(Perfect Forwarding)
当你第一次了解到Move语义和完美转发的时候,它们看起来非常直观:
- Move语义使编译器有可能用廉价的Move操作来代替昂贵的复制操作。正如复制构造函数和复制赋值操作符给了你赋值对象的权利一样,Move构造函数和Move赋值操作符也给了控制Move语义的权利。Move语义也允许创建(move-only)的类型,例如std::unique_ptr, std::future 和 std::thread。
- 完美转发使接收任意数量参数的函数模板成为可能,它可以将参数转发到其他的函数,使目标函数接收到的参数与被传递给转发函数的参数保持一致。
右值引用是连接这两个截然不同的概念的胶合剂。它隐藏在语言机制之下,使移动语义和完美转发变得可能。
23. 理解std::move
和std::forward<T>
std::move
无条件的把类型转换成右值std::forward
只把绑定了右值的参数转换成右值- 这两个工具只是类型转换函数,不会产生任何代码
std::forward
典型的用在模板函数中, 将通用引用的参数原样传给其它函数
std::move
实现原理
1 | template<typename T> |
这货就是强制转换, 函数
std::move
要返回的是一个右值引用。但是如果T的类型恰好是一个左值引用,T&&的类型就会也会是左值引用。所有要用type_traits
的remove_reference
去除T
类型的引用。
std::forward<T>
实现原理
1 | // 利用引用折叠特性, 见条款28 |
条款2提到过, 在通用引用参数的函数中,调用参数和模板T的关系如下:
1
2
3
4
5
6
7
8
9 int x=27;
const int cx = x;
const int& rx = x;
template<typename T> void f(T&& param);
f(x); // 参数是 int&, T是int&
f(cx); // 参数是 const int&, T是const int&
f(rx); // 参数是 const int&, T是const int&
f(27); // 参数是 int&&, T是int, <--注意这里, 靠它完美转发
std::forward<T>
依据传入的T模板参数确定是否转换,当T是值类型时,才会转换右值。所以实际上std::forward<T>
也可以当作std::move
来使用,只要给出正确的模板参数。
std::forward<T>
使用场景
1 | void process(const Widget& lvalArg); |
24. 区分通用引用
- 如果函数模板参数是
T&&
且推导类型就是T, 或者是用auto&&
声明的对象, 这是通用引用. - 如果模板类型的推导是有限制的, 那么这是右值引用(见下文解释)
- 通用引用被左值初始化它就是左值引用,被右值初始化它就是右值引用.
先看一下面的代码压压惊
1 | void f1(Widget&& param); // 右值引用 |
f2函数中的参数类型推导是有范围限制的, param不可能是
std::vector<T>&&
以外的其它类型, 所以这是右值引用.
f3函数的参数相反, T没有限制, 所以这是通用引用.
同样的原则, 下面是更复杂的例子
1 | template<class T, class Allocator=alloctor<T>> |
25. 右值引用使用std::move
, 通用引用使用std::forward<T>
理由: 你肯定不想把传入通用引用的左值给move
了。
26. 避免重载通用引用函数
理由: 编译器选择的可能和我们想的不一样
27. 熟悉通用引用重载的替代方法
- 避免重载, 见26条
- 老实的回到C++98, 用
const T&
- 传值, 比如
MyClass(std::string s)
- 利用TAG来区分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18template<typename T>
void logAndAdd(T&& name)
{
logAndAddImpl(
std::forward<T>(name),
std::is_integral<typename std::remove_reference<T>:type>());
}
template<typename T>
void logAndAddImpl(T&& name, std::false_type)
{
//...
}
void logAndAddImpl(int idx, std::true_type)
{
//...
} - 在通用引用上用
std::enable_if
约束模板, 这个…打扰了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Person {
public:
template<
typename T,
typename = std::enable_if_t<
!std::is_base_of<Person, std::decay_t<T>>::value
&&
!std::is_integral<std::remove_reference_t<T>>::value
>
>
explicit Person(T&& n)
: name(std::forward<T>(n))
{...}
explicit Person(int idx)
: name(nameFromIdx(idx))
{...}
} - 权衡, 用通用引用通常有效率优势, 但是增加了复杂度, 使得可用性下降
28. 理解引用折叠(reference collapsing)
- 引用折叠发生在下面四个情形中:模板实例化、auto类型生成、使用typedef或声明别名、decltype
- 引用的引用被折叠成一个引用,如果有一个是左值引用,那么结果就是左值引用
- 通用引用并不是一种新的引用类型,它的行为可以用引用折叠来解释
29. 不要对move操作的性能报过高的期望
- 有些容器无法简单移动,如
std::array
- 有些类的优化可能使拷贝速度优于移动,如字符串长度较短的
std::string
30. 熟悉完美转发不适用的场合
- 大括号初始化列表
- 用NULL作为指针
- 位字段
- 模板函数名、重载函数名
- 类中声明的静态常量