Effective Modern C++ 笔记 - 右值引用, Move语义和完美转发(Perfect Forwarding)

当你第一次了解到Move语义和完美转发的时候,它们看起来非常直观:

  • Move语义使编译器有可能用廉价的Move操作来代替昂贵的复制操作。正如复制构造函数和复制赋值操作符给了你赋值对象的权利一样,Move构造函数和Move赋值操作符也给了控制Move语义的权利。Move语义也允许创建(move-only)的类型,例如std::unique_ptr, std::future 和 std::thread。
  • 完美转发使接收任意数量参数的函数模板成为可能,它可以将参数转发到其他的函数,使目标函数接收到的参数与被传递给转发函数的参数保持一致。

右值引用是连接这两个截然不同的概念的胶合剂。它隐藏在语言机制之下,使移动语义和完美转发变得可能。

23. 理解std::movestd::forward<T>

  • std::move无条件的把类型转换成右值
  • std::forward只把绑定了右值的参数转换成右值
  • 这两个工具只是类型转换函数,不会产生任何代码
  • std::forward典型的用在模板函数中, 将通用引用的参数原样传给其它函数

std::move实现原理

1
2
3
4
5
6
template<typename T>
decltype(auto) move(T&& param)
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}

这货就是强制转换, 函数std::move要返回的是一个右值引用。但是如果T的类型恰好是一个左值引用,T&&的类型就会也会是左值引用。所有要用type_traitsremove_reference去除T类型的引用。

std::forward<T>实现原理

1
2
3
4
5
6
// 利用引用折叠特性, 见条款28
template<typename T>
T&& forward(remove_reference_t<T>& param)
{
return static_cast<T&&>(param);
}

条款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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void process(const Widget& lvalArg);
void process(Widget&& rvalArg);

template<typename T>
void logAndProcess(T&& param)
{
auto now = std::chrono::system_clock::now();
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param));
}

// 调用logAndProcess时, 正确转发参数
Widget w;

logAndProcess(w); // call with lvalue
logAndProcess(std::move(w)); // call with rvalue

24. 区分通用引用

  1. 如果函数模板参数是T&&且推导类型就是T, 或者是用auto&&声明的对象, 这是通用引用.
  2. 如果模板类型的推导是有限制的, 那么这是右值引用(见下文解释)
  3. 通用引用被左值初始化它就是左值引用,被右值初始化它就是右值引用.

先看一下面的代码压压惊

1
2
3
void f1(Widget&& param);      // 右值引用
template<typename T> void f2(std::vector<T>&& param); // 右值引用
template<typename T> void f3(T&& param); // 通用引用

f2函数中的参数类型推导是有范围限制的, param不可能是std::vector<T>&&以外的其它类型, 所以这是右值引用.
f3函数的参数相反, T没有限制, 所以这是通用引用.

同样的原则, 下面是更复杂的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class T, class Allocator=alloctor<T>>
class vector {
public:
void push_back(T&& x); // 这里的T是有约束的(被vector的定义约束), 是右值引用

template<class ...Args>
void emplace_back(Args&&... args); // 这里的Args没有约束, 是通用引用
};

auto funcor = [](auto&& func, auto&&... args) // 没有约束, 是通用引用
{
std::forward<decltype(func)>(func)(
std::forware<decltype(args)>(args)...
);
};

25. 右值引用使用std::move, 通用引用使用std::forward<T>

理由: 你肯定不想把传入通用引用的左值给move了。

26. 避免重载通用引用函数

理由: 编译器选择的可能和我们想的不一样

27. 熟悉通用引用重载的替代方法

  1. 避免重载, 见26条
  2. 老实的回到C++98, 用const T&
  3. 传值, 比如MyClass(std::string s)
  4. 利用TAG来区分
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    template<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)
    {
    //...
    }
  5. 在通用引用上用std::enable_if约束模板, 这个…打扰了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class 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))
    {...}
    }
  6. 权衡, 用通用引用通常有效率优势, 但是增加了复杂度, 使得可用性下降

28. 理解引用折叠(reference collapsing)

  1. 引用折叠发生在下面四个情形中:模板实例化、auto类型生成、使用typedef或声明别名、decltype
  2. 引用的引用被折叠成一个引用,如果有一个是左值引用,那么结果就是左值引用
  3. 通用引用并不是一种新的引用类型,它的行为可以用引用折叠来解释

29. 不要对move操作的性能报过高的期望

  1. 有些容器无法简单移动,如std::array
  2. 有些类的优化可能使拷贝速度优于移动,如字符串长度较短的std::string

30. 熟悉完美转发不适用的场合

  1. 大括号初始化列表
  2. 用NULL作为指针
  3. 位字段
  4. 模板函数名、重载函数名
  5. 类中声明的静态常量