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

  1. 针对万能引用类别进行重载会导致形形色色的问题,解决这些问题的办法有:
    舍弃重载,但是不能解决完美转发构造函数的问题,并且彻底放弃重载也不行
    传递const T&类型的形参,使用左值常量引用类型来代替传递万能引用类型,缺点在于达不到我们想要的高效率
    传值:传值不会出现让调用者出乎意料的情况
  2. 如果又不想放弃重载,又不想放弃万能引用,只要让函数模板中不止包含万能引用这一形参,另外非万能引用形参只要具备充分差的匹配能力,就足以让这个函数模板不满足精确匹配,这个想法就是标签分派手法的基础。
  3. 运用“标签”的唯一目的在于强制重载决议时按我们想要的方向进行推进,这些形参在运行期不起任何作用,针对万能引用函数模板内的重载实现函数发起的调用把工作“分派”到正确的重载版本的手法就是创建合适的标签对象,这种设计因而得名:标签分派。
  4. 对于完美转发构造函数,由于编译器自己生成复制和移动构造函数的原因,有时也会调用编译器生成的版本,因此如果在完美转发构造函数使用标签分派并不能保证其会不会绕过标签分派系统,因此标签分配就不能满足我们的需要了。
  5. std::enable_if可以让我们把含有万能引用部分的函数模板被允许采用的条件砍掉一部分,可以强制编译器表现出来的行为如同特定的模板不存在一般,这样的模板称为禁用的。
     class Person{
         public:
         template<typename T,typename=typename std::enable_if<condition>::type>
         explicit Person(T&& n);
         ...
     }
    
  6. std::is_same可以判断两个类型是否相同,但是引用饰词和cv饰词的加入会使比较结果不同,即使他们都是整型,一个是引用,另一个不是引用比较结果就是false。std::decay<T>::type和T相同,区别在于它移除了T的引用和cv饰词(即const和volatile饰词),也可以用于把数组和函数类别强制类型转换成指针类型。
  7. is_same比较派生类和基类时结果肯定是false,因此此时派生类调用Person基类的构造函数还是会调用万能引用构造函数,为了解决这一问题,std::is_base_of可以判定一个类型是否由另一个类型派生而来。若T2由T1派生而来,std::is_base_of<T1,T2>::value是真,而且所有的std::is_base_of<T,T>::value都为真,因为所有类型都可以认为是从自身派生而来。所以使用std::is_base_of代替std::is_same能得到我们想要的结果。
  8. 如果要加上其余限制,可以在enable_if里加上其余条件即可,可以使用&&   等关系运算符
  9. 万能引用形参通常在性能方面具备优势,但在易用性,比如传递了非法形参等问题时报错信息可能会让人摸不着头脑。

理解引用折叠

  1. 实参在传递给函数模板时,推导出来的模板形参会将实参是左值还是右值的信息编码到结果类型中,这个编码操作只有在实参被用以初始化的形参时万能引用时才会发生。
  2. 如果传递的实参是个左值,T的推导结果就是左值引用类型,如果传递的实参是个右值,T的推导结果就是非引用类型。
  3. 声明和定义“引用的引用”是违法的,但是编译器在特殊的语境下会产生引用的引用。当编译器生成引用的引用时就会触发引用折叠机制。引用折叠出现的语境有四种:
    (1)模板实例化
    (2)auto变量的类型生成
    (3)生成和使用typedef和别名声明
    (4)decltype的运用
  4. 如果引用的引用出现在上述语境中,该双重引用会折叠成单个引用,规则如下:
    如果任一引用为左值引用,则结果为左值引用,否则(即两个皆为右值引用),结果为右值引用
  5. 引用折叠是std::forward得以运作的关键。下面为C++14中std::forward可能的实现
     template<typename T>
     T&& forward(remove_reference_t<T>& param){
         return static_cast<T&&>(param);
     }
    

    传递的param是左值就返回左值引用,右值就返回右值引用。

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

  1. 如果类型并不提供对移动的显式支持,也不符合编译器生成移动操作的条件,当然也就没有期待其在C++11下相比C++98有任何性能提升了。
  2. 并不是所有的移动操作都是廉价的,比如C++11中新引入的容器std::array,它实质上就是带有STL接口的内建数组。std::array类型的对象其内容都是直接存储在对象内的。
  3. 作为对比,string类型采用小型字符串优化(small string optimization,SSO)在实施了基于SSO的实现的前提下,对小型字符串实施移动并不比复制更快。小型字符串会存储在std::string对象内的某个缓冲区内,而不去使用堆上分配的存储。
  4. 在这样几个场景中C++11的移动语义不会带来什么好处:
    (1)没有移动操作:待移动的对象未能提供移动操作,因此移动操作变成了复制请求
    (2)移动未能更快:待移动的对象虽有移动操作,但并不比其复制操作更快
    (3)移动不可用:移动本可以发生的语境下,要求移动操作不可发生异常,但该操作未加上noexcept声明
    (4)源对象是个左值,除了极少数例外(局部对象用于返回值优化的少部分情况),只有右值可以作为移动操作的源。

熟悉完美转发的失败情形

  1. 完美转发的含义是我们不仅转发对象,还转发其显著特征:类型、是左值还是右值、以及是否带有const或volatile饰词等,因此我们需要使用万能引用。
  2. 给定目标函数f和转发函数fwd,当以特定实参调用f会执行某操作,而用同一实参调用fwd会执行不同的操作,则称完美转发失败。
  3. 不能实施完美转发的实参有:大括号初始化物,在f的直接调用中,编译器先领受了调用端的实参类型,又接受了f所声明的形参类型,编译器会比较这俩类型是否兼容,如有必要将实施隐式类型转换来使得调用得以成功。而经由转发函数模板fwd来对f实施间接调用,编译器就会采用采用推导的手法来取得实参的类型,然后再比较类型推导结果和f声明的形参类型。
  4. 完美转发会在下面两个条件中的任何一个成立时失败:
    • 编译器无法为一个或多个fwd的形参推导出结果。在此情况下,代码无法编译通过
    • 编译器为一个或多个fwd的形参推导出了“错误的”类型结果,这里错误的既可以指fwd根据类型推导结果的实例化无法通过编译,也可以指以fwd推导出的类型调用f与直接传递给fwd的实参调用f行为不一致,这种情况可能原因是f是个重载函数,由推导出来的类型调用了其他的重载版本。
  5. 0和NULL用作空指针,0和NULL都不是指针类型,因此类型推导出来的肯定不是指针类型,因此0和NULL都不能用作空指针以进行完美转发。
  6. 仅有声明的整型static const成员变量,因为万能引用的底层就是指针,但是仅有声明的整型static const是无法取地址的,因此若想进行完美转发需要对其进行定义即可。
  7. 重载的函数名字和模板名字,作为一个函数模板,没有任何关于类型需求的信息,这也使得编译器不可能决定应该传递哪个函数重载版本,特别是传递另一个函数模板。
  8. 位域,非const的引用无法绑定到位域,位域是由机器字的若干部分组成的,但是这样的实体是不可能有办法对其直接取址的。因此可以传递位域的仅有的形参类型就是按值传递和常量引用(reference-to-const),常量引用不可能绑定到位域,它们绑定到的是“常规”对象,其中复制了位域的值。