23:理解std::movestd::forward

  • std::move:返回变量的右值引用
    • 对const对象的移动操作会被转换为拷贝操作
      • 因为const对象经过std::move会返回一个const右值引用,而一般函数重载的移动版本形参都是非const的右值引用,无法匹配
    • std::move不移动对象,而且也不保证对象一定被移动,仅仅返回对象的右值引用
    1
    2
    3
    4
    5
    6
    
    template<typename T>             // C++14
    decltype(auto) move(T&& param)   
    {
      using ReturnType = remove_reference_t<T>&&;
      return static_cast<ReturnType>(param);
    }
    
  • std::forward:实现完美转发(保持对象的左值性或右值性)
    • 通常情况下,形参总是左值,即使其类型是右值引用
  • std::movestd::forward只是进行类型转换,在运行时不做任何事
  • 参考

24:区分万能引用和右值引用

万能引用和右值引用只是形式上类似,但这是两个概念

  • 万能引用:形式为T&&auto&&,并且存在类型推导
    • 函数模板参数:template <typename T> void func(T&& param);
    • auto类型推导:auto&& val = myVal;
      1
      
      auto myFunc = [] (auto&& func, auto&&... params) {/* do something */}
      
  • 右值引用
    • 带const(不是纯粹的T&&形式):template <typename T> void func(const T&& param);
    • 形式是T&&,但是不存在类型推导:比如vector的push_back,但是emplace_back中参数是万能引用
      1
      2
      3
      4
      5
      6
      7
      8
      
      template <typename T, typename Allocator = allocator<T>>
      class vector{
          public:
              void push_back(T&& x); // 调用push_back时,类型T已知
      
              template <typename... Args> 
              void emplace_back(Args&&... args); // 参数包args的类型Args独立于T,存在类型推导,这里是万能引用
      }
      
  • 参考

25:针对右值引用实施std::move,针对万能引用实施std::forward

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Widget{ // 以例子来说明
public:
    Widget(Widget&& rhs): name(std::move(rhs.name)), sp(std::move(rhs.sp)) {} // 形参为右值引用,将形参(左值)进行移动

    template <typename T>
    void setName(T&& newName){ // 形参为万能引用,保持形参的左值性或右值性
        cout<<"set new name:"<<newName<<endl; 
        name = std::forward<T>(newName); // 在函数中使用move或forward时,使用的位置应该是该参数最后一次使用的时候
    }

    Widget operator+(Widget&& lhs, Widget&& rhs){
        lhs.name += rhs.name;
        return std::move(lhs);
    }

    template <typename T>
    T doNothing(T&& t) { return std::forward<T>(t); }    
private:
    string name;
    shared_ptr<vector<int>> sp;
};
  • 重载setName不是一个好的设计
    • 可能效率低:如果传入字面量,即使匹配到右值版本的函数,形参仍然会作为临时对象
    • 如果有多个参数,需要重载$2^N$种,如果使用参数包,则无法实现
  • 在函数中使用move或forward时,使用的位置应该是该参数最后一次使用的时候
  • 如果函数中将形参进行处理,然后返回
    • 传值返回:如果形参是右值引用(比如Widget operator+成员函数),使用move返回;如果形参是万能引用(比如doNothing成员函数),使用forward返回
    • 如果返回值是函数中的局部变量,则编译器有特定的优化:RVO
  • 返回值优化RVO(Return Value Optimization):减少函数返回时产生临时对象,进而消除部分拷贝或移动操作
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    // 原来
    Widget func() { return Widget(); } // 有一次默认构造,一次拷贝构造
    Widget w = func(); // 再加上一次拷贝构造
    
    // 使用RVO优化,上面过程相当于:
    void func(Widget& w) { w.Widget::Widget(); } // Widget w在外面分配空间,直接传入func中进行构造,因此只需要一次(默认)构造
    
    // NRVO(Named Return Value Optimization)原理类似
    Widget func() {
        Widget w;
        return w; // 返回对象已经具名
    }
    
    • 使用前提:局部对象的类型和返回值类型相同,而且局部对象就是返回值
    • 限制场景:
      • 返回std::move():默认构造+移动构造
      • 进行赋值而非初始化Widget w; w = func();:默认构造+func中的默认构造和拷贝构造
      • 不同的分支条件下,返回不同的局部对象
  • 参考

26:避免依万能引用类型进行重载

  • 原因:函数匹配规则
    • 如果模板实例化出的函数和普通重载函数都精确匹配,则优先选择普通重载函数,其次选择模板函数实例化出来的精确版本
  • 例子
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Person{
    public:        
        explicit Person(int idx): _name(nameFromIdx(idx)) {} 
        
        template <typename T> // 对Person(int)的重载
        explicit Person(T&& name): _name(std::forward<T>(name)) {}

    private:
        std::string _name;
};

short id = 1;
Person p1(id); // 会调用模板实例化的版本,而非进行类型转换调用普通版本
const Person p2(id); 

// 这个情况极其容易混淆,
Person q2(p2); // 会调用生成的拷贝构造函数(因为其实参为const Person&)
Person q1(p1); // 会调用模板实例化的版本,而非调用生成的拷贝构造函数

// 尤其当Person作为基类,派生类在构造函数中初始化基类部分时
class SpecialPerson: public Person{
    public:
        SpecialPerson(const SpecialPerson& rhs): Person(rhs) {}
        SpecialPerson(SpecialPerson&& rhs): Person(std::move(rhs)) {}
        // 这两个构造函数均使用基类Person构造函数的完美转发版本,
}

// 对万能引用参数的函数进行重载,不是一个好的设计

27:熟悉依万能引用类型进行重载的替代方案

  • 放弃重载,使用不同的函数名

    • 但是对于构造函数就无能为力
  • 普通函数形参为const type&类型

    • 因此传入const实参,会优先使用原来的普通版本,而非重载的万能引用版本
  • 将形参从引用类型换成值类型:当知道肯定要复制形参时,考虑按值传递

    1
    2
    3
    4
    5
    6
    7
    
    class Person{
    public:
        explicit Person(std::string name): _name(std::move(name)) {}
        explicit Person(int idx): name(nameFromIdx(idx)) {}
    private:
        std::string _name;
    }
    
  • 使用Tag分发:使用Tag对参数进行区分,进而分发到不同的函数实现

    • 背景:如果想使用完美转发,就必须要使用万能引用
    • 例子:
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      
      // 两个函数实现的版本
      template <typename T> void logAndAddImpl(T&& name, std::false_type) {}
      template <typename T> void logAndAddImpl(int idx, std::true_type) {}
      // 使用Tag对参数进行区分
      template <typename T> void logAndAdd(T&& name) {
          logAndAddImpl(
              std::forward<T>(name), 
              std::is_integral<typename std::remove_reference<T>::type>()
          ); // 或者C++14:std::is_integral<typename std::remove_reference_t<T>
      }
      
      • 如果传入true or false,到运行时才能决定
      • 在编译阶段进行模板匹配,std::is_integral在编译阶段就可以判断类型是否为整型
  • 约束接受万能引用的模板:std::enable_if判断

    • 背景:构造函数无法使用Tag分发
    • 例子:
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      
      #include <type_traits>
      class Person{
      public:
          explicit Person(int idx): _name(nameFromIdx(idx)) {}
      
          template<typename T, 
                  typename = std::enable_if_t<
                      !std::is_base_of_v<Person, std::decay_t<T>> 
                      &&
                      !std::is_integral_v<std::remove_reference_t<T>>                    
                  >            
          > // 当类型T不为Person或者其派生类,抑或T不为int型时,才会选择这个的重载版本,使用万能引用进行重载并实现完美转发
          explicit Person(T&& name): _name(std::forward<T>(name)) {
              static_assert(
                  std::is_constructible(std::string, T)::value, 
                  "Parameter name can't be used to construct a std::string"
              ); // 验证类型为std::string的对象能否被类型为T的对象构造
          }
      
      private:
          std::string _name;
      };
      
      • std::enable_if<condition>::type:只有满足条件的模板才会使用(C++14std::enable_if_t
      • std::is_same<T1, T2>::value(C++17std::is_same_v
      • std::is_base_of<T1, T2>::value:如果T2继承于T1,则为true;且std::is_base_of<T, T>::value==true(C++17std::is_base_of_v
      • std::decay<T>::type的类型与T的类型相同,忽略了引用、const、volatile(C++14std::decay_t
  • 权衡

    • 前三种方案都需要对需要调用的函数形参逐一指定其类型,后两种方案使用万能引用实现了完美转发
    • 虽然完美转发效率更高(避免创建临时对象),但是某些对象无法实现完美转发,并且使用完美转发并编译报错时,报错信息的可读性很差
      • std::is_constructible可以在编译期测试一个类型的对象能否被另一个不同类型的对象(或者多个不同类型的多个对象)构造,因此可以用来验证转发函数的万能引用参数是否合法
  • 参考

28:理解引用折叠

  • 几种引用折叠的应用场景:
    • 万能引用的实例化:在模板类型推导时,可能出现“引用的引用”的情况,此时需要用到引用折叠
    • std::forward完美转发:
      1
      2
      3
      4
      
      template <typename T>
      T&& forward(typename remove_reference<T>::type& param){
          return static_cast<T&&>(param);
      }
      
    • auto类型推导,decltype类型推导
    • typedef类型别名
  • 参考

29:假定移动操作不存在、成本高、未使用

  • 几种移动语义不可用、不高效的情况:
    • 没有移动操作:编译器只有在没有用户自定义拷贝操作和析构函数时,才自动生成移动操作
    • 移动未能更快:
      • std:array
        • 一般STL中容器的对象都分配在堆上,对象中有指向堆上内存的指针,因此移动操作只需要进行指针的更新、源对象的指针置空即可
        • 但是std::array中内容分配在栈上(栈上的数组),移动操作等于复制操作
      • std::string
        • std::string针对小对象有一个优化SSO(Small String Optimization),小对象直接存储在栈上而非堆上,省去动态内存分配
    • 移动不可用:移动操作没有标记为noexcept
      • 如果移动操作没有标记为noexcept,即使是适合使用移动操作的场景,编译器也会使用复制操作替代
    • 源对象是左值:只有右值可以作为移动操作的源(左值可以用,但是很容易造成空悬问题)
  • 参考

30:熟悉完美转发的失败情形

  • 完美转发的含义:不仅转发对象,而且转发其特征(左值、右值、const、volatile)
  • 完美转发的失败情形
    • 列表初始化
      1
      2
      3
      4
      5
      6
      7
      8
      
      void f(const std::vector<int>& v) {}
      template <typename T> void fwd(T&& param) {}
      
      f({1,2,3}); // ok
      fwd({1,2,3}); // 编译报错:无法推断出T的类型
      
      auto il = {1,2,3};
      fwd(il); // T=initializer_list<int>
      
    • 0或NULL作为空指针
      • 0或NULL会被推导为int型而非空指针类型,因此完美转发后得到的类型是int,但是形参是指针类型
    • 仅仅声明整型的静态常量数据成员
      1
      2
      3
      4
      5
      
      class Widget{
      public:
          static cosnt int cnst = 12; // 声明而非定义,不会分配实际的存储空间,而是常量传播(直接将用到cnst的地方替换为12)
      };
      fwd(Widget::cnst); // 编译报错:找不到cnst的定义
      
      • 只声明不会分配空间,因此无法取地址,也无法使用引用,不能使用完美转发
      • 解决方法:在类外或是对应.cpp文件中添加定义:const int cnst = 12;
    • 函数重载和函数模板
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      
      void f(int (*pf)(int)); 
      int func(int a);
      int func(int a, int b);
      f(func); // ok
      fwd(func); // 模板类型推导失败:无法确定是哪个重载版本
      
      // 解决方法:
      using FuncType = int (*)(int);
      fwd(static_cast<FuncType>(func));
      // 但是万能引用和完美转发一般是针对任意类型的,这里限定了类型,语义与实现矛盾
      
    • 位域:位域只是int类型的一部分,没有一个确切地址,也就无法引用
  • 参考