[施工中][c++11] move semantics && perfect forwarding 学习笔记

背景

move semantics是modern cpp中非常重要的特性,有必要详细了解一下。

updateime: 2022年7月2日

move semantic

基本的内容大家都很熟悉,就不说了

std::move 做了什么

std::move没有move任何内容,只是简单把传进来转换为对应的rvalue reference

实现为:

1
2template<typename T>
3constexpr std::remove_reference_t<T>&& move(T&& t) noexcept
4{
5    return static_cast<std::remove_reference_t<T>&&>(t);
6}
Tip

需要强调的是,无论传入的是const or non-const, lvalue or rvalue, ref or non-ref, std::move都会无条件(无差别)地转换为 rvalue refernece

被std::move的值处于一个什么状态

Tip

moved from object is in a valid but unspecified state

说人话就是,对于moved from object,仍然可以对其进行一些操作,但是其state是未知的

我们拆开来看

这里提到的一些操作,其实只有三种:

  • 析构函数
  • move assigment operator
  • copy assigment operator

其余操作都是非法的。

为什么state是未知的呢?因为这和实现有关 标准只要求moved from object是可以被析构的,但是内部是什么值,其实是不确定的。 参考 cpp core guidelines C.64

Ideally, that moved-from should be the default value of the type. Ensure that unless there is an exceptionally good reason not to. However, not all types have a default value and for some types establishing the default value can be expensive. The standard requires only that the moved-from object can be destroyed. Often, we can easily and cheaply do better: The standard library assumes that it is possible to assign to a moved-from object. Always leave the moved-from object in some (necessarily specified) valid state.

此外,由于moved from object在出了所在的scope后就会调用析构函数,因此称之为一个"expiring value(Xvalue)",也就是临近生命周期终点的"将亡值". 详细可以参考 浅谈 Cpp Value Categories

when not to use std::move

最常见的一种情景是,在函数return 时错误地使用了std::move,从而阻止了编译器可能的copy elision

1
2T fn()
3{
4  T t;
5  return std::move (t);
6}

实际上,编译器会先去尝试做copy elision,如果无法做到,那么会去implicit std::move

Warning

因此,无论如何都不要把std::move 用在函数返回值上

forwarding reference

其实就是universal refernce. forward reference是官方名称

1
2template<typename T>
3void f(T&& x);   // forwarding reference
4
5auto && var2 = var1;  // forwarding reference
6
7

forwarding refernece

  • 被lvalue初始化就是lvalue reference
  • 被rvalue初始化就是rvalue reference

forwarding reference 从语法上看起来和rvalue reference 还是比较相像的,那如何区分呢? 如下情况时,实际上是forwarding reference而不是rvalue reference

  • 发生了type deduction
  • 从形式上看,出现了T&&或者auto&&

perfect forwarding

完美转发,也叫精确传递,指的是“需要将一组参数原封不动的传递给另一个函数”。

原封不动的含义是说:数值不变,左值/右值属性不变,const/non-const属性不变。

在泛型函数中,这样的需求非常普遍。

比如下面这个例子:

 1    
 2    template <typename T> void forward_value(const T& val) { 
 3     process_value(val); 
 4    } 
 5    template <typename T> void forward_value(T& val) { 
 6     process_value(val); 
 7    }
 8    
 9    int a = 0; 
10     const int &b = 1; 
11     forward_value(a); // int& 
12     forward_value(b); // const int& 
13    forward_value(2); // int&
14

在右值引用出现,模板的每个参数都必须重载两个类型,总的重载次数是指数级的orz,警察听了想打人

然而有了右值引用,不管模板右多少个参数,我们只需要定义一次,接受一个右值引用的参数,就能够将所有的参数类型原封不动的传递给目标函数。

 1    
 2    template <typename T> void forward_value(T&& val) { 
 3     process_value(val); 
 4    }
 5    
 6    int a = 0; 
 7    const int &b = 1; 
 8    forward_value(a); // int& 
 9    forward_value(b); // const int& 
10    forward_value(2); // int&&
11
12

那么最后说回std::move. std::move的作用就是传入左值,返回右值引用,从而使用右值引用带来的好处(节省资源,实现完美转发等)

 1    #include <iostream>
 2    #include <utility>
 3    #include <vector>
 4    #include <string>
 5     
 6    int main()
 7    {
 8        std::string str = "Hello";
 9        std::vector<std::string> v;
10     
11        // 使用 push_back(const T&) 重载,
12        // 表示我们将带来复制 str 的成本
13        v.push_back(str);
14        std::cout << "After copy, str is \"" << str << "\"\n";
15     
16        // 使用右值引用 push_back(T&&) 重载,
17        // 表示不复制字符串;而是
18        // str 的内容被移动进 vector
19        // 这个开销比较低,但也意味着 str 现在可能为空。
20        v.push_back(std::move(str));
21        std::cout << "After move, str is \"" << str << "\"\n";
22     
23        std::cout << "The contents of the vector are \"" << v[0]
24                                             << "\", \"" << v[1] << "\"\n";
25    }
26    After copy, str is "Hello"
27    After move, str is ""
28    The contents of the vector are "Hello", "Hello"
29

参考资料:

Posts in this Series