• 技术书籍 — EffectiveMordenCpp 研读


    一、类型推导

    PROs:

    • 源码某处的类型修改,可以自动传播其他地方

    Cons:

    • 会让代码更复杂(How?)

      • 在模板类型推导时,有引用的实参会被视为无引用,他们的引用会被忽略

        template<typename T>
        void f(T & param);  // param 是一个引用
        
        int x = 10;  // int
        const int cx= x; // const int
        const int &rx = cx; // const int &
        
        f(x);    // T = int, param 类型为 int&
        f(cx);   // T = const int, param 类型为 const int&, 常量性被保留
        f(rx);   // T = const int, param 类型为 const int&, 引用被忽略
        
      • 对于通用引用的推导,左值实参会被特殊对待(T都被推导为了左值引用

        template<typename T>
        void f(T && param);  // 通用类型引用,两个&
        
        f(x);   // x 为左值,T = int&, param 类型为 int&
        f(cx);  // cx 为左值, T = const int&, param 类型为 const int&
        f(rx);  // rx 为左值, T = const int&, param 类型为 const int&
        f(10);  // 10 为右值, T = int, param 类型为 int&&
        
      • 对于传值类型推导,实参如果具有常量性const和易变性volitate会被忽略

        template<typename T>
        void f(T param);    // param 是一份拷贝
        
        f(x);    // T = param = int
        f(cx);   // T = param = int,const 被忽略了,因为 const 不会(也不应该)传播到拷贝上
        f(rx);   // T = param = int,const 被忽略了,因为 const 不会(也不应该)传播到拷贝上
        
        const char* const ptr = "const string";  // ptr 为常量指针,且指向常量
        

      f(ptr); // T = char, param = char const,仅指针的常量性被忽略了

      
      + 在模板类型推导时,数组或者函数实参会**退化为指针**,除非他们被用于初始化引用
      
      ```cpp
      const char name[] = "I am John.";  // name 为 const char[11]
      const char* pname = name;   // 数组退化为指针
      
      template<typename T>
      void f1(T param);
      
      f1(name);  // T = const char*,这里的常量性不会被移除么?
      
      template<typename T>
      void f2(T& param);
      
      f2(name);   // T = const char[11], param 类型为 const char(&)[11]
      
      // 用途:编译器推导数组大小常量
      template<typename T, std::size_t N>
      constexpr std::size_t arraySize(T(&)[N]) noexcept {
        return N;
      }
      
      void bar(int, float);
      
      f1(bar);   // param 的类型为: void(*)(int, float)
      f2(bar);   // param 的类型为: void(&)(int, float)
      
      

    二、move和forward

    要点:当传递给函数我右值引用时,应该无条件转换为右值(使用std::move);通用引用应该有条件转换为右值(使用std::forward

    举个栗子:

    class Widget {
      public:
        // rhs 为一个右值引用,使用std::move
      	Widget(Widget&& rhs): name(std::move(rhs.name)), p(std::move(rhs.p)) {}
      
        template<typename T>
        void setName(T&& newName){ // newName是一个通用引用
          name = std::forward<T>(newName); // 使用std::forward
        }
      private:
      	std::string name;
        std::shared_ptr<Data> p;
    };
    

    在上述栗子中,通用引用可能绑定到有资格移动的对象上。当使用右值初始化时,std::forward才会将其强制转化为右值

    扩展场景一:

    在使用按值返回的函数,且返回值绑定到右值引用或通用引用,需要对返回值使用std::movestd::forward

    // Case 1: std::move
    // 左侧的参数也用于保存计算的结果
    Matrix operator+(Matrix&& lhs, const Matrix& rhs){
      lhs += rhs;
      return std::move(lhs); //lhs 可以移动到返回值时的内存位置
      // return lhs; 会被编译器copy到返回值的内存空间
    }
    
    // Case2: std::forward
    template<typename T>
    Fraction reduceAndCopy(T&& frac){
      frac.reduce();
      /*
       *1. 如果 frac 是一个右值,则会直接移动到返回值中,避免copy开销
       *2. 如果 frac 是一个左值,则必须创建副本
       */
      return std::forward<T>(frac);
      // return frac; // 总是会创建副本
    }
    

    扩展场景二:

    开发者可能会误用 std::move,比如下面的栗子。

    Widget makeWidget(){
      Widget w;
      ...;
      return w; // 编译器会自动进行RVO(返回值优化)
      //return std::move(w); // 不要这样做!
    }
    

    RVO(返回值优化)的条件:

    1. 局部变量与返回值的类型相同
    2. 局部变量就是返回值
    /*
     * 1. 此处返回的已经不是局部对象 w,而是w的引用
     * 2. 返回 w 的引用不符合RVO的第二个条件
     * 3. 试图帮助编译器优化,反而限制了优化效果
     */
    return std::move(w);
    

    三、熟悉通用引用重载的替代方法

    方式一:Pass by Value

    将按引用传递参数替换为按值传递(这违反直觉)。

    class Person{
      public:
         // 替换原生的 T&&
         explicit Person(std::string s): name(std::move(s)){}
         explicit Person(int idx): name(nameFromIdx(idx)){}
      private:
      	 std::string name;
    };
    

    方式二:使用Tag dispatch

    首先回顾一下存在重载问题的case代码:

    std::multiset<std::string> names;
    template<typename T>
    void logAndAdd(T&& name)
    {
      auto now = std::chrono::system_clock::now();
      log(now, "logAndAdd");
      // 当 name 是一个int类型的重载,则会导致names.emplace(int)错误
      names.emplace(std::forward<T>(name));
    }
    

    解决方案:

    template<typename T>
    void logAndAdd(T&& name)
    {
      // 借助一个 bool tag实现重载函数的分发
      logAndAddImpl(std::forward<T>(name),
                   std::is_integeral<typename std::remove_reference<T>::type>());
      // 不直接使用std::is_integeral<T>()的原因是:is_inteageral<T&>会返回FALSE,对应传入左值的情况
    }
    
    template<typename T>
    void logAndAddImpl(T&& name, std::false_type)
    {
      names.emplace(std::forward<T>(name));
    }
    
    // 特化版本实现
    std::string nameFromIdx(int idx);
    void logAndAdd(int idx, std::true_type)
    {
      logAndAdd(nameFromIdx(idx));
    }
    

    方法三:约束使用通用引用模板

    要点:搭配使用 std::enable_ifstd::decaystd::is_base_of

    class Person
    {
      public:
      // 模板应该在 T 不是Person的时候启用
        template<typename T, typename=std::enable_if<!std::is_base_of<Person, std::decay<T>>::value>>
        explicit Person(T&& p);
    };
    

    使用 std::enable_if来选择性禁止Person通用引用构造器,以使得一些参数确保使用移动或copy构造函数。

    四、引用折叠

    引用折叠发生在四种情况:

    • 模板实例化
    • auto变量的类型生成
    • typedef
    • decltype
    Widget w;
    // w1为一个左值引用,auto推导出来的为 Widget&
    // 代入即为:Widget& && w1 = w; 引用折叠后为 Widget& w1 = w;
    auto&& w1 = w; 
    

    通用引用不是一种新的引用,它实际上是满足两个条件下的右值引用:

    • 通过类型推导将左值和右值区分。
      • T 类型的左值被推导为&类型,T类型的右值被推导为T
    • 引用折叠的发生

    typedef的样例:

    template<typename T>
    class Widget
    {
      public:
      	typedef T&& RvalueRefToT;
    };
    
    Widget<int&> w;
    /*推导-->*/ typedef int& && RvalueRefToT;
    /*引用折叠-->*/ typedef int& RvalueRefToT;
    

    这清楚地表明 typedef 有时可能并不按照我们预期的设置生效的。

    五、完美转发的失败case

    std::forward只适用于通用引用场景,在按值传递和指针参数并不适用。

    如下是常用的场景:

    template<typename T>
    void fwd(T&& param)
    {
      f(std::forward<T>(param));
    }
    
    template<typename... Ts>
    void fwd(Ts&&... params)
    {
      f(std::forward<T>(param)...);
    }
    

    失败case一:Braced initializers(支撑初始化器)

    void f(const std::vector<int>& v);
    
    f({1,2,3});// ok, 隐式转换
    fwd({1,2,3}); // compile error;
    // 原因:推导传入fwd的参数类型,将其与f的声明类型比较
    
    auto il = {1, 2, 3};
    fwd(il); // ok
    
    • 当编译器不能推导出一个或者多个fwd参数类型,编译器会报错
    • 当编译器将一个或者多个fwd参数类型推导错误。

    失败case二:0或者NULL作为空指针

    0或者NULL作为空指针给模板时,类型推导只会推导出整型,而不是一个指针类型。建议使用nullptr代替

    失败case三:仅声明的整数静态const数据成员

    class Widget
    {
      public:
        static const std::size_t MinVals = 28;
    };
    std::vector<int> widgetData;
    widgetData.reserve(Widget::MinVals); //ok
    
    f(Widget::MinVals);//ok
    fwd(Widget::MinVals);//可以编译,但link error
    
    const std::size_t Widget::MinVals; // 加上定义即可
    

    六、智能指针

    要点一:unique_ptr

    // TODO

  • 相关阅读:
    自己实现的一个简单的C# IOC 容器
    C# 内存缓存工具类 MemoryCacheUtil
    使用触发器和C#程序实现数据同步
    Maven 命令安装指定 jar 包到本地仓库
    C# RSA 非对称加密
    JS可选链操作符?.和双问号??
    Learn D3 入门文档: Introduction
    Lerna 基本概念
    图片 src 为二进制的处理
    ASCII 和 Base64
  • 原文地址:https://www.cnblogs.com/CocoML/p/14643409.html
Copyright © 2020-2023  润新知