• 【读书笔记】【Effective Modern C++】型别推导


    条款 1:理解模板型别推导

    • 该条款的前置知识为 C++ 表达式的值类别:

      • 如下图所示:
        cpp表达式值类型

      • 名词解释如下:

        • glvalue,全称 generalized lvalue,⼴义左值。
        • rvalue,通常只能放在等号右边的表达式,右值。
        • lvalue,通常可以放在等号左边的表达式,左值。
        • xvalue,全称 expiring lvalue,将亡值。
        • prvalue,全称 pure rvalue,纯右值。
      • 介绍左值 lvalue:

        • 左值是有标识符、可以取地址的表达式。
          • 变量、函数或数据成员的名字。
          • 返回左值引⽤的表达式,如 ++xcout << ' '
          • 字符串字⾯量如 "hello world"
        • 在函数调⽤时,左值可以绑定到左值引⽤的参数,如 T&;⼀个常量只能绑定到常左值引⽤,如 const T&
      • 介绍纯右值 prvalue:

        • 纯右值是没有标识符、不可以取地址的表达式。【例如临时对象】
          • 返回⾮引⽤类型的表达式,如 x++x+1make_shared(42)
          • 除字符串字⾯量之外的字⾯量,如 42true
        • 在 C++11 之前:
          • 右值可以绑定到常左值引⽤(const lvalue reference)的参数,如 const T&,但不可以绑定到⾮常左值引⽤(non-const lvalue reference),如 T&
        • 在 C++11 之后:
          • C++ 语⾔⾥多了⼀种引⽤类型⸺右值引⽤,右值引⽤的形式是 T&&,⽐左值引⽤多⼀个 & 符号。
          • 跟左值引⽤⼀样,我们可以使⽤ constvolatile 来进⾏修饰。
    • 模板的型别推导是 auto 的基础。【注意下面代码中,ParamType 其实是和 T 相关的】

      // 函数模板大致形如:
      template<typename T>
      void f(ParamType param); // ParamType 通常与 T 相关
      
      // 它的一次调用形如:
      f(expr);// 以某表达式调用f
      
      // 在编译期,编译器会通过expr推导两个型别:
      	// 一个是T的型别,另一个是ParamType的型别,这两个型别往往不一样
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    • 注意上述代码中,T 的型别推导结果,不仅仅依赖 expr 的型别,还依赖 ParamType 的形式,具体要分下述三种情况讨论。【ParamType 是形参类型,expr 是传参类型】

    • 情况一,ParamType 具有指针或引用型别,但不是万能引用(universal reference)。【万能引用将在条款 24 中介绍】

      1. expr 具有引用型别,先将引用部分忽略。【包括指针】
      2. 然后对 expr 的型别和 ParamType 的型别执行模式匹配,来决定 T 的型别。
      3. ParamType 具有引用特性时,代码分析如下:【ParamType 是指针时也是差不多的结果】
        // 假设模板如下:
        template<typename T>
        void f(T& param) {} // ParamType是个引用,此处就是 T&
        
        // expr的型别:
        int x = 27; // x的型别是int
        const int cx = x; // cx的型别是const int
        const int& rx = x; // rx是x的型别为const int的引用
        
        // ParamType的型别:
        f(x); // T的型别是int, param的型别是int&
        f(cx); // T的型别是const int, param的型别是const int&
        f(rx); // T的型别是const int, param的型别是const int&, 注意:即使rx具有引用型别,T也并未被推导成一个引用,原因在于,rx的引用性(reference-ness)会在型别推导过程中被忽略
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
      4. 上述代码有多个值得注意的地方:
        • 首先,当向引用型别的形参传入 const 对象时,常量性会被保留。【即当 ParamType 是引用时,对该模板传入 const 对象很安全,连 T 也会保留其常量性】
        • 其次,如前所述,expr 的引用性被忽略了。
      5. 而当 ParamType 具有 const 引用特性时,代码分析如下:【这里和前面比较只有一个小地方不同】
        // 假设模板如下:
        template<typename T>
        void f(const T& param) {} // ParamType是个引用,此处就是 const T&
        
        // expr的型别:
        int x = 27; // x的型别是int
        const int cx = x; // cx的型别是const int
        const int& rx = x; // rx是x的型别为const int的引用
        
        // ParamType的型别:
        f(x); // T的型别是int, param的型别是const int&
        f(cx); // T的型别是int, param的型别是const int& 【只有这里有一点不同,因为 param 本身就是const,所以T的推导不需要保留 const】
        f(rx); // T的型别是int, param的型别是const int&
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
    • 情况二,ParamType 是一个万能引用。【也就是 T&&】【该部分的内容在条款 24 中会有更详细的解释】

      1. 如果 expr 是左值,则 TParamType 都会被推导为左值引用。【这里可以看出,型别推导只有一种特殊情况,只有在这种情况下,T 具有引用特性】
      2. 如果 expr 是右值,则按照常规来推导,所谓的常规情况就是情况一。
      3. 代码分析如下:
        // 假设模板如下:
        template<typename T>
        void f(T&& param) {} // ParamType是个万能引用
        
        // expr的型别:
        int x = 27; // x的型别是int
        const int cx = x; // cx的型别是const int
        const int& rx = x; // rx是x的型别为const int的引用
        
        // ParamType的型别:
        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(27); // 27是右值,T的型别是int,param的型别是int&&
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
    • 情况三,ParamType 既非指针也非引用,也就是按值传递。

      1. 代码分析如下:
        // 假设模板如下:
        template<typename T>
        void f(T param) {}
        
        // expr的型别:
        int x = 27; // x的型别是int
        const int cx = x; // cx的型别是const int
        const int& rx = x; // rx是x的型别为const int的引用
        
        // ParamType的型别:
        f(x); // T的型别是int, param的型别是int
        f(cx); // T的型别是int, param的型别是int
        f(rx); // T的型别是int, param的型别是int
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
      2. 无论传入的 expr 是什么,param 都是一个全新对象,也就是一个副本。
      3. 也就是说,Tparam 的推导都是一样的,都会忽略 exprconst 特性、& 特性和 volatile 特性。
    • 在本条款讨论的内容中,还需要继续讨论数组实参带来的影响。

      • 虽然在普通情况中,数组会退化成指针,但是在型别推导中,数组型别和指针型别还是需要区分的。
      • 代码分析如下:
        const char str[] = "abcde"; // name的型别为 const char[6]
        const char* ptr = str; // 数组退化为指针
        
        // 假设模板如下:
        template<typename T>
        void f1(T param) {} // 按值传递
        f1(str); // str是个数组,退化为指针后按值传递,因此 T 的型别为 const char*
        
        // 奇怪的点在于如果 param 具有引用形式,情况会有所不同:
        template<typename T>
        void f2(T& param) {} // ParamType 是 T&
        f2(str); // 此时会传递一个数组,因此 T 的型别为 const char[6],同时 param 的型别为 const char& [6]
        // 注意上面 T 的推导中带有了数组的长度
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
      • 该部分中关于数组和指针的内容也适用于函数和指针。
    • 总结:

      • 在型别推导中,实参的引用性会被忽略。
      • 对万能引用形参进行推导,左值实参会带来特殊情况。
      • 对按值传递的形参进行推导,实参中的 const 特性、volatile 特性都会被忽略。
      • 数组或函数如果面对引用形参则会保持原类型,否则(按值传递)就会退化为指针。

    条款 2:理解 auto 型别推导

    • auto 型别推导其实可以等价于模板型别推导,代码分析如下:【auto 推导的情况和条款 1 中模板推导的情况是一致的(包括数组和函数退化成指针的情况)】
      // 模板型别推导如下:
      template<typename T>
      void f(ParamType param); // ParamType 通常与 T 相关
      // 它的一次调用形如:
      f(expr);// 以某表达式调用f
      // 在编译期,编译器会通过expr推导 T 和 ParamType
      
      // auto 的使用如下:
      auto x = 27; // x 型别饰词为 auto
      const auto cx = x; // cx 型别饰词为 const auto
      const auto& rx = x; // rx 型别饰词为 const auto&
      
      // auto 与 函数模板推导的等价如下:【只是概念上等价,并非说编译器真的生成了】
      template<typename T>
      void func_for_x(T param); 
      func_for_x(27); // 推导 27 的 T 和 ParamType
      
      template<typename T>
      void func_for_cx(const T param); 
      func_for_cx(x);
      
      template<typename T>
      void func_for_rx(const T& param); 
      func_for_rx(x);
      
      // 其他符合的情况可以看书中具体例子
      
      • 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
    • 只有一种情况下,auto 型别会有例外,那就是 auto 与**初始化列表(initializer list)**的结合,具体代码如下:
      auto x1 = 27;   // 型别是 int,值是27
      auto x2(27);    // 同上
      auto x3 = {27}; // 型别是 std::initializer_list,值是27
      auto x4{27};    // 同上
      
      • 1
      • 2
      • 3
      • 4
    • 初始化列表的使用相当于在 auto 型别推导和模板型别推导中加多一个中间层。
      • 需要注意的是,如果 std::initializer_list 中的 T 推导失败,则 auto 的推导也会失败,因此大括号里的值需要保持一致的型别。
      • 还有一点需要注意的是,std::initializer_list 其实就是一个和大括号“绑定”的型别(依我的理解是这样),如果想在普通的模板型别推导中使用,需要明确使用 std::initializer_list 而不是 T。【具体看书中代码】
    • 最后要讨论的是在 C++14 中添加的内容:如果在函数返回值或者 lambda 式的形参中使用 auto 关键字,其实是在使用普通的模板型别推导,也就是说这两种情况底下不可以使用大括号。

    条款 3:理解 decltype

    • C++11 中,decltype 的主要用途大概就在于声明那些返回值型别依赖于形参型别函数模板。【通常情况下就是有什么就返回什么】
    • 使用 decltype 来计算返回值型别的代码如下:【可以认为 decltype 的使用就是将代码的意图展露出来】
      // 这段代码就是 decltype 的 c++11 用法
      template<typename Container, typename Index>
      auto authAndAccess(Container& c, Index i)    // 注意这里的 auto 并不会引起条款 2 中的 auto 型别推导
        ->decltype(c[i]) // 这里使用了C++11中的返回值型别尾序语法,也就是 `->` 和 `decltype` 和 `auto` 使用
        // 注意 decltype 的括号里用的是形参
      {
        authenticateUser();
        return c[i];  // 这里容器的 operator[] 通常会返回 T&
      }
      // operator[]返回值是什么型别,authAndAccess的返回值就是什么型别,和我们期望的结果一致
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    • C++11 允许对单表达式的 lambda 式的返回值型别实施推导,而 C++14 则将这个允许返回扩张到了一切 lambda 式和一切函数,包括那些多表达式的。
      • 对于 authAndAccess 这种情况来说,这就意味着在 C++14 中可以去掉返回值型别尾序语法,而只保留前导 auto
      • 在那样的声明形式中,auto 确实说明会发生型别推导,具体地说,它说明编译器会依据函数实现来实施函数返回值的型别推导:
        template<typename Container, typename Index>    //C++14;
        auto authAndAccess(Container& c, Index i)       //不甚正确
        {
          authenticateUser();
          return c[i];  //返回值型别是根据c[i]推导出来,这里 operator[] 通常是返回 T&
        }
        
        // 上述代码看起来没有问题,但是下面使用不能通过编译:
        vector<int>vec = { 1,2,3 };
        authAndAccess(vec, 2) = 6; // 关键在于 authAndAccess 会消去 T[] 带来的引用性(符合条款 1 中的第三种情况),所以函数的返回值是一个右值,不能被赋值,所以不能通过编译
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
    • 所以 autodecltype 的结合是很有必要的,如前面所述,decltype 的使用能帮助返回值找到本身类型。
    • 但是在 C++14 中有了新的用法,即 decltype(auto) 饰词,代码如下:
      // 这段代码就是 decltype 的 c++14 用法
      // 对于C++14来说,可以直接支持返回值为decltype(auto)进行推导,而C++11只能使用尾序推导进行推导
      template<typename Container, typename Index>   //C++14;
      decltype<auto>                                 //能够运行
      authAndAccess(Container& c, Index i)           //但仍需改进
      {
          authenticateUser();
          return c[i];
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    • 注意上述 C++14 标准下的 authAndAccess(Container& c, Index i) 函数后面标注着仍需要改进,其实就是考虑将 Container& c 中的左值引用改为万能引用 Container&& c,这种时候如何修改返回值 c[i] 使得结果能被推导呢?答案如下:【使用 std::foward
      // 本段代码的修改是为了让authAndAccess采用一种既能绑定到左值也能够绑定到右值的引用形参。
      // C++11 标准下代码如下:
      template<typename Container, typename Index>   //C++11最终版
      auto                                 
      authAndAccess(Container&& c, Index i)        // 注意到 Index i 是按值传递
      ->decltype(std::forward<Container>(c)[i])  
      {
          authenticateUser();
          return std::forward<Container>(c)[i];
      }
      // C++14 标准下代码如下:
      template<typename Container, typename Index>   //C++14最终版
      decltype<auto>      // decltype 用的是 decltype 的推导规则
      authAndAccess(Container&& c, Index i)          
      {
          authenticateUser();
          return std::forward<Container>(c)[i];
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
    • 本条款的最后讨论下 decltype 与普通表达式的结合情况,将 decltype 应用于一个名字之上,就会得出该名字的声明型别。
    • 通常情况下就是推导,但是有个特别情况需要注意,问题代码如下:
      // 正常情况:
      decltype(auto) f1()
      {
        int x = 0; // x 是一个临时变量
        ...
        return x;    //decltype(x)是int,所以f1返回的是int
      }
      
      // 产生问题的情况:
      decltype(auto) f2()
      {
          int x = 0; // x 是一个临时变量
          ...
          return (x);   //decltype((x))是int&,所以f2返回的是int&
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
    • 在上面代码中,x 变量是一个左值;表达式 (x) 也是一个左值,是一个比仅有名字更复杂的表达式;此时 decltype 会将结果推导为 T&
      • 也就是说,对于复杂表达式,decltype 会推断为 T&,而非 T
      • 因此上述问题代码返回了一个临时变量的引用。
    • 最后总结一下:
      • auto 遵循模板参数推导规则,总是推导出一个对象类型。
      • decltype(auto) 遵循 decltype 规则,根据值类别推导出引用类型。
      • 代码如下:
        int x;
        int && f();
        
        // expression    auto       decltype(auto)
        // ----------------------------------------
        // 10            int        int
        // x             int        int
        // (x)           int        int &
        // f()           int        int &&
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9

    条款 4:掌握查看型别推导结果的方法

    • 最关键的是理解 C++ 型别推导规则,其实就是理解条款 1 至条款 3。
  • 相关阅读:
    外卖跑腿小程序开发如何满足不断变化的用户需求?
    R语言判别分析
    我只认两种产业互联网形态
    【云原生 | 从零开始学Kubernetes】二十、Service代理kube-proxy组件详解
    目标检测论文解读复现之二十:基于改进Yolov5的地铁隧道附属设施与衬砌表观病害检测方法
    icepak求解报错“internal error in fan domain error argument not in valid range”
    nodejs使用es-batis
    Mysql笔记
    如何通俗理解海涅定理
    虚拟化环境内存管理
  • 原文地址:https://blog.csdn.net/weixin_44705592/article/details/127647849