闭关之 C++ Template 笔记(二):PartⅠ基本概念(二)

目录

第六章 移动语义与 enable_if<>

6.1 完美转发

  • C++11 引入了特殊的规则对参数进行完美转发
    template<typename T>
    void f (T&& val) 
    {
      g(std::forward<T>(val)); // perfect forward val to g()
    }
    
  • std::move 没有模板参数,并且会无条件地移动参数
  • std::forward<> 会跟据被传递参数的具体情况决定是否“转发”其潜在的移动语义
  • 模板参数 T 的 T&&和具体类型 X 的 X&&是不同的
    • 具体类型 X 的 X&&
      • 右值引用
    • 模板参数 T 的 T&&
      • 声明了一个转发引用(万能引用)
      • 可以被绑定到可变、不可变(比如 const)或者可移动对象上
  • 完美转发程序示例
    • Code_6_1_1

6.2 特殊的成员函数模板

  • 特殊成员函数也可以是模板
    • Code_6_2_1
    • 对于一个非 const 左值的 Person p,成员模板通常比预定义的拷贝构造函数更匹配
      template<typename STR>
      Person(STR&& n)
      
      Person (Person const& p)
      
      ...
      
      Person p3(p1); //报错
      
      • 可以额外提供一个非 const 的拷贝构造函数
      • 更好的办法还是使用模板
        • 通过 std::enable_if<> 实现

6.3 使用 enable_if<>禁用模板

  • C++11 开始,通过 C++b标准库提供的辅助模板 std::enable_if<>,可以在某些编译期条件下忽略掉函数模板
  • 示例
    template<typename T>
    typename std::enable_if<(sizeof(T) > 4)>::type
    foo() 
    {
    }
    
    • sizeof(T) > 4 不成立的时候被忽略掉
    • 如果成立,函数模板会展开成
      template<typename T>
      void foo() 
      {
      }
      
  • std::enable_if<> 是一个 type trait,它计算作为其(第一个)模板参数传递的给定编译期表达式
    • 行为如下
      • 如果这个表达式结果为 true,它的 type 成员会返回一个类型
        • 如果没有第二个模板参数,返回类型是 void
        • 否则,返回类型是其第二个参数的类型
      • 如果表达式结果 false,则其成员类型是未定义的,包含 std::enable_if<> 的函数模板被忽略掉
  • C++14 开始所有的type traits 都返回一个类型,因此可以使用一个与之对应的别名模板 std::enable_if_t<>, 使用该别名模板可以省略掉 template::type
    template<typename T>
    std::enable_if_t<(sizeof(T) > 4)>
    foo() 
    {
    }
    
  • std::enable_if<>std::enable_if_t<> 传递第二个模板参数
    template<typename T>
    std::enable_if_t<(sizeof(T) > 4), T>
    foo() 
    {
      return T();
    }
    
    • 结果为 true 返回值类型为 T
  • std::enable_if_t<(sizeof(T) > 4), T> 放在声明的中间不够优美
    • 可以使用一个额外带有默认值的模板参数优化
      template<typename T, 
              typename = std::enable_if_t<(sizeof(T) > 4)>>
      void foo() 
      {
      }
      
    • 如果条件成立会被展开成
      template<typename T, typename = void>
      void foo() 
      {
      }
      
  • 如果还是觉得不够优美可以使用别名模板
    template<typename T>
    using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;
    
    template<typename T, typename = EnableIfSizeGreater4<T>>
    void foo()
    {
    }
    

6.4 使用 enable_if<>

  • 使用 enable_if<>可以解决 6.2 节中关于构造函数模板的问题
  • 需解决的问题
    • 当传递的模板参数的类型不正确时,禁用构造函数模板
  • 使用 std::is_convertiable<FROM, TO>
  • 示例
    template<typename STR,
             typename = std::enable_if_t<std::is_convertible_v<STR, std::string>>>
    Person(STR&& n);
    
  • 如果 STR 可以转换成 std::string,扩展为
    template<typename STR, typename = void>
    Person(STR&& n);
    
  • 否则函数模板会被忽略
  • 使用别名模板优化
    template<typename T>
    using EnableIfString = std::enable_if_t<std::is_convertible_v<T,std::string>>;template<typename STR, typename = EnableIfString<STR>>
    Person(STR&& n);
    
  • 示例代码
    • Code_6_4_1
  • 除了使用要求类型之间可以隐式转换的 std::is_convertible<> 外,还可以使用 std::is_constructible<>
    • 用显式转换来做初始化
      template<typename T>
      using EnableIfString =std::enable_if_t<std::is_constructible_v<std::string, T>>;
      
    • 注意参数顺序与 std::is_convertible<> 相反
  • 禁用特殊成员函数
    • 不能通过使用 enable_if<> 禁用拷贝和移动构造函数以及赋值构造函数
      • 因为成员函数模板不会被算作特殊成员函数
      • 因此,即使定义相应的成员函数模板,但需要拷贝构造函数的地方依然会使用预定义的拷贝构造函数
    • 解决办法
      • 定义一个接受 const volatile 的拷贝构造函数并将其设置为delete
        • 这样做就不会再隐式声明一个接受 const 参数的拷贝构造函数
      • 再定义一个构造函数模板
        • 对于 nonvolatile 的类型,该构造函数模板会优选被选择
        class C
        {
        public:// user-define the predefined copy constructor as deleted
          // (with conversion to volatile to enable better matches)
          C(C const volatile&) = delete;
          // implement copy constructor template with better match:
          template<typename T>
          C (T const&)
          {
            std::cout << "tmpl copy constructor\n";
          }};
        
      • 使用
        • C y{x}; // uses the member template
    • 经由上述的改动,是就可以给该构造函数模板添加 enable_if<> 限制
    • 如禁止对通过 int 类型参数实例化出来的 C<> 模板实例进行复制
      template<typename T>
      class C
      {
      public:// user-define the predefined copy constructor as deleted
        // (with conversion to volatile to enable better matches)
        C(C const volatile&) = delete;
        // if T is no integral type, provide copy constructor templatewith better match:
        template<typename U,
                typename = std::enable_if_t<!std::is_integral<U>::value>>
        C (C<U> const&) 
        {}};
      

6.5 使用 Concepts 简化 enable_if<> 表达式

  • 示例
    template<typename STR>
    requires std::is_convertible_v<STR,std::string>
    Person(STR&& n) 
      : name(std::forward<STR>(n)) 
    {}
    
  • 模板的使用条件定义成通用的 concept
    template<typename T>
    concept ConvertibleToString = std::is_convertible_v<T,std::string>;
    
  • 使用 concept 用作模板条件
    template<typename STR>
    requires ConvertibleToString<STR>
    Person(STR&& n) 
      : name(std::forward<STR>(n)) 
    {}
    
  • 可以写成
    template<ConvertibleToString STR>
    Person(STR&& n) 
      : name(std::forward<STR>(n))
    {}
    

第七章 按值传递还是按引用传递?

  • X const &(const 左值引用)
    • 参数引用了被传递的对象,但参数不能被更改
  • X &(非 const 左值引用)
    • 参数引用了被传递的对象,但参数可以被更改
  • X &&(右值引用)
    • 参数通过移动语义引用了被传递的对象,参数值可以被更改或被“窃取”。

7.1 按值传递

  • 当按值传递参数时,原则上所有的参数都会被拷贝
  • 编译器可以通过移动语义优化对象的拷贝,即使是对复杂类型的拷贝,其成本也不会很高
  • std::string s = "hi"; printV(s);
    • 被传递的参数是左值(lvalue),因此拷贝构造函数会被调用
  • printV(std::string("hi"));
    • 被传递的参数是纯右值, 此时编译器会进行优化,使拷贝构造函数不会被调用
    • 纯右值
      • prvalue:pure right value
        • 临时对象或某个函数的返回值
  • std::string returnString(); printV(returnString());
    • 同上,不会调用拷贝构造函数
  • C++17 开始,C++ 标准要求对纯右值的优化方案必须被实现
    • 如果编译器没有优化这类拷贝,应尝试使用移动语义
  • printV(std::move(s));
    • 被传递参数是 xvalue
      • xvalue
        • 使用了 std::move() 的已经存在的非 const 对象
        • 通知编译器不再需要 s 的值
        • 从而强制调用移动构造函数
  • 按值传递的类型退化(decay)
    • 在模板中,当按值传递参数时,参数类型会退化(decay)
      • 原始数组会退化成指针
      • const 和 volatile 等会被删除
      • 就像用一个值去初始化一个用 auto 声明的对象那样
  • 示例
    template<typename T>
    void printV (T arg) {}
    
    std::string const c = "hi";
    printV(c);    //1  c decays so that arg has type std::string
    printV("hi"); //2  decays to pointer so that arg has type char const*
    int arr[4];
    printV(arr);  //3  decays to pointer so that arg has type int *
    
    • 代码 2 使用的模板实例化成 void printV (char const* arg) { … }

7.2 按引用传递

7.2.1 按常引用传递

  • 为了避免任何(不必要的)拷贝,在传递非临时对象参数时,可以使用 const 引用
  • 当引用对象被修改,也就是更改该地址指向的内容时。编译器就要假设在这次调用之后,所有缓存在寄存器中的值可能都会变为无效。但重新载入值可能会很耗时
    • 可能比拷贝对象的成本高很多
  • 对 inline 的函数,情况可能会好一些
    • 如果编译器可以展开 inline 函数,那么它就可以根据调用者和被调用者的信息,推断出该地址中的值是否会被更改。
    • 函数模板通常总很短,因此很可能会被作为 inline 展开。但如果模板中有复杂的算法,那么大概率就不会被展开
  • const_cast 移除参数中的 const
  • 按引用传递不会做类型退化
    template<typename T>
    void printR (T const& arg) {}
    
    std::string const c = "hi";
    printR(c);    // T deduced as std::string, arg is std::string const&
    printR("hi"); // T deduced as char[3], arg is char const(&)[3]
    int arr[4];
    printR(arr);  // T deduced as int[4], arg is int const(&)[4]
    

7.2.2 按非常量引用传递

  • 函数模板按非常量引用传递参数时,如果传递的参数是 const 的(如:const int
    • 模板参数的类型就可能被推断为 const 引用,这时就可以传递一个右值作为参数
    • 但此时模板所期望的是左值类型参数,因而会发生错误
  • 禁止非 const 引用传递 const 对象的做法
    • 使用 static_assert 触发一个编译期错误
      template<typename T>
      void outR (T& arg) 
      {
        static_assert(!std::is_const<T>::value, "out parameter of foo<T>(T&)is const");}
      
    • 使用 std::enable_if<> 禁用模板
      template<typename T,
               typename = std::enable_if_t<!std::is_const<T>::value>
      void outR (T& arg) 
      {}
      
    • 使用 concepts 禁用模板
      template<typename T>
      requires !std::is_const_v<T>
      void outR (T& arg) 
      {}
      

7.2.3 按转发传递 Forwarding Reference

  • 可以将任意类型的参数传递给转发引用
    • 与按引用传递类似,都不会创建被传递参数的备份
  • 当使用一个未初始化的对象传递给按转发传递的模板时,会将模板参数 T 隐式推断为引用,会触发错误
    template<typename T>
    void passR(T&& arg)  // arg is a forwarding reference
    {
      T x; // for passed lvalues, x is a reference, which requires an initializer…
    }
    
    foo(42); // OK: T deduced as int
    int i;
    foo(i); // ERROR: T deduced as int&, which makes the declaration ofxin passR() invalid
    
  • 处理这类状况在 15.6.2 小节

7.3 std::ref() 和 std::cref() 的使用

  • C++11 开始可以让调用者自己决定向函数模板传递参数使用值传递还是引用传递
    • 当模板参数被声明成按值传递时
      • 可以使用 <functional> 中的 std::ref()std::cref() 将参数按引用传递给函数模板
        template<typename T>
        void printT (T arg) {}
        
        std::string s = "hello";
        printT(s); //pass s By value
        printT(std::cref(s)); // pass s “as if by reference”
        
    • std::cref() 没有改变函数模板内部处理参数的方式
      • 模板按值传递
  • std::ref()std::cref() 思想
    • 创建一个 std::reference_wrapper<> 对象
    • 该对象引用原始对象,并被按值传递给函数模板
  • std::reference_wrapper<> 只支持
    • 向原始类型的隐式类型转换,该转换返回原始对象
    • 当需要操作被传递对象时,可以直接使用 std::reference_wrapper<> 对象
    • 编译器必须知道 std::reference_wrapper<> 对象的原始参数类型,才会进行隐式转换
      • 因此只有在泛型代码传递对象时才能正常使用
      • 如果尝试直接操作传递进来的类型为 T 的对象,可能会报错,因为 std::reference_wrapper<> 没有定义各类操作符

7.4 处理字符串字面量常量与原始数组

7.4.1 字符串字面量常量和原始数组的特殊实现

  • 对数组参数和指针参数做不同的实现
  • 为了区分上述两种情况,需检测被传递进来的参数是不是数组。通常有两种方法
    • 模板定义成只能接受数组作为参数
      template<typename T, std::size_t L1, std::size_t L2>
      void foo(T (&arg1)[L1], T (&arg2)[L2])
      {
        T* pa = arg1; // decay arg1
        T* pb = arg2; // decay arg2
        if (compareArrays(pa, L1, pb, L2)) 
        {}
      }
      
    • 可以使用 type traits 来检测参数是不是数组
      template<typename T, 
              typename = std::enable_if_t<std::is_array_v<T>>>
      void foo (T&& arg1, T&& arg2)
      {}
      
  • 建议:字符串和原始数组
    • 由于上述特殊的处理方式过于复杂,最好还是使用专门的函数模板处理数组参数
    • 使用 std::vectorstd::array 作为函数模板的参数
  • 只要是字符串和原始数组,必须对它们进行单独考虑

7.5 处理返回值

  • 返回值也可以按引用或按值返回
  • 按引用返回的情况
    • 返回容器或字符串中的元素
    • 允许修改类对象的成员
    • 链式调用返回对象
  • 对成员的只读访问,返回 const 引用
  • 要确保函数模板采用按值返回的方法有两种
    • 用 type trait std::remove_reference<> 将 T 转为非引用类型
      template<typename T>
      typename std::remove_reference<T>::type retV(T p)
      {
        return T{}; // always returns by value
      }
      
    • 将返回值类型声明为 auto, 让编译器推断返回值类型
      • auto 也会导致类型退化
      template<typename T>
      auto retV(T p) // by-value return type deduced by compiler
      {
        return T{}; // always returns by value
      }
      

7.6 模板参数声明的建议

  • 函数模板有多种传递参数的方式
    • 按值传递
      • 会对字符串字面量和原始数组的类型进行退化
      • 对较大的对象可能影响性能
        • 可以通过 std::cref()std::ref() 按引用传递参数
    • 按引用传递
      • 较大的对象能够提供比较好的性能, 适用如下情况
        • 将已经存在的对象(lvalue)按左值引用传递
        • 将临时对象(prvalue)或被 std::move() 转换为可移动的对象(xvalue)按右值引用传递
        • 或将以上几种类型的对象按照转发引用传递
  • 在传递字面量和原始数组时要格外小心
  • 转发引用需要注意
    • 模板参数可能会被隐式推断为引用类型
  • 对于函数模板有如下建议
    • 默认情况下,将参数声明为按值传递
      • 这样做比较简单并且字符串字面量也可以正常工作
      • 比较小的对象、临时对象及可移动对象,其性能较好
      • 比较大的对象,为了避免拷贝,可以使用 std::ref()std::cref()
    • 如果有充分的理由,可以这样做
      • 需要参数用于输出,或即用于输入也用于输出,就将该参数按非 const 引用传递
        • 但需按照 7.2.2 小节介绍的方法禁止其接收 const 对象
      • 为了转发参数,就使用完美转发
        • 将参数声明为转发引用,并在合适的地方使用 std::forward<>
        • 使用 std::decay<>std::common_type<> 处理不同的字符串字面量类型及原始数组类型的情况
      • 重点考虑程序性能,参数拷贝的成本很高,使用 const 引用
        • 如果最终还是要对对象进行局部拷贝的话,该建议不适用
    • 如果经验丰富,可不遵循这些建议。
      • 但不要仅凭直觉对性能做评估
      • 在这方面专家也会犯错
  • 不要过分泛型化(Over-Generic)
    • 在实际应用中,函数模板并不是为了所有可能的类型定义的
    • std::make_pair<> 示例
      • 是一个很好的介绍参数传递机制相关缺陷的例子
      • C++98 使用按引用传递来避免不必要的拷贝
        template<typename T1, typename T2>
        pair<T1,T2> make_pair (T1 const& a, T2 const& b)
        {
          return pair<T1,T2>(a,b);
        }
        
        • 当存储不同长度的字符串字面量或原始数组时会导致严重的问题
      • C++03 该函数模板被定义成按值传递参数
        template<typename T1, typename T2>
        pair<T1,T2> make_pair (T1 a, T2 b)
        {
          return pair<T1,T2>(a,b);
        }
        
      • C++11 需要支持移动语义,使用转发引用
        • 大致实现如下
        template<typename T1, typename T2>
        constexpr pair<typename decay<T1>::type, typename decay<T2>::type>
        make_pair (T1&& a, T2&& b)
        {
          return pair<
                  typename decay<T1>::type, 
                  typename decay<T2>::type
                  >(forward<T1>(a), forward<T2>(b));
        }
        
  • C++ 标准库在很多地方都使用了类似的方法对参数进行完美转发
    • 会结合 std::decay<> 使用
  • 当性能非常重要时,不要相信直觉,要进行测试

第八章 编译期编程

  • C++ 支持编译期编程的特性
    • C++98 模板就有了编译期计算的能力
      • 循环和执行路径选择
    • 在编译期间,通过部分特化在类模板的不同实现之间做选择
    • 根据 SFINAE 原则,可以根据限制条件和要求,在函数模板的不同实现间做选择
    • C++11 支持 constexpr,能更直观的选择执行路径
    • C++14 支持更多的语法 for 循环,switch 语句等
    • C++17 引入了编译期 if(compile-time if)
      • 可以根据某些编译期条件或限制弃用某些语句
      • 也可以用非模板函数

8.1 模板元编程

  • 模板的实例化发生在编译期间
  • C++ 模板的某些特性可以和实例化过程相结合,产生一种 C++ 自己内部原始递归 “编程语言”
  • 在编译期间能判断一个数是不是质数
    template<unsigned p, unsigned d> // p: number to check, d: currentdivisor
    struct DoIsPrime 
    {
        static constexpr bool value = (p % d != 0) && DoIsPrime<p, d - 1>::value;
    };
    
    template<unsigned p> // end recursion if divisor is 2
    struct DoIsPrime<p, 2> 
    {
        static constexpr bool value = (p % 2 != 0);
    };
    
    
    template<unsigned p> // primary template
    struct IsPrime 
    {
        // start recursion with divisor from p/2:
        static constexpr bool value = DoIsPrime<p, p / 2>::value;
    };
    
    
    // special cases (to avoid endless recursion with templateinstantiation):
    template<>
    struct IsPrime<0> { static constexpr bool value = false; };
    template<>
    struct IsPrime<1> { static constexpr bool value = false; };
    template<>
    struct IsPrime<2> { static constexpr bool value = true; };
    template<>
    struct IsPrime<3> { static constexpr bool value = true; };
    
    • 表达式 IsPrime<9>::value 编译期展开步骤
      • DoIsPrime<9,4>::value
      • 9%4!=0 && DoIsPrime<9,3>::value
      • 9%4!=0 && 9%3!=0 && DoIsPrime<9,2>::value
      • 9%4!=0 && 9%3!=0 && 9%2!=0
      • 由于 9%3 == 0,返回 false

8.2 使用 constexpr 进行计算

  • C++11 引入 constexpr 特性
    • 简化了各种类型的编译期计算
    • C++11 对 constexpr 函数的使用限制大部分在 C++14 移除
    • 不支持堆内存分配和异常
  • 判断一个数是不是质数
    • Code_8_2_1
    • 如果 b2 定义在全局作用域或命名空间内,会在编译期进行计算
    • 如果 b2 定义在块作用域内,将由编译器决定是否在编译期间进行计算

8.3 局部特化的执行路径选择

  • 在编译期间通过部分特例化在不同的实现间做选择
  • 广泛应用于基于模板参数属性
    • 在不同模板实现间做选择
  • 示例
    // primary helper template:
    template<int SZ, bool = isPrime(SZ)>
    struct Helper;
    
    // implementation if SZ is not a prime number:
    template<int SZ>
    struct Helper<SZ, false>
    {};
    
    // implementation if SZ is a prime number:
    template<int SZ>
    struct Helper<SZ, true>
    {};
    
    template<typename T, std::size_t SZ>
    long foo (std::array<T,SZ> const& coll)
    {
      Helper<SZ> h; // implementation depends on whether array has primenumber as size}
    
    • 根据参数 std::array<>size 是不是一个质数,给出两种 Helper<> 模板的实现
    • 对两种可能的情况实现了两种偏特化版本模板
  • 也可以将主模板用于其中一种情况,然后再特化另一种情况版本
    // primary helper template (used if no specialization fits):
    template<int SZ, bool = isPrime(SZ)>
    struct Helper
    {};
    
    // special implementation if SZ is a prime number:
    template<int SZ>
    struct Helper<SZ, true>
    {};
    
  • 函数模板不支持部分特化,必须要用其它方法
    • 使用有 static 函数的类
    • 使用 std::enable_if
    • 使用 SFINAE 特性
    • 使用编译期的 if

8.4 SFINAE (替换失败并不是一种错误)

  • Substitution Failure Is Not An Error
    • 编译期实例化模板时,要进行模板的选择和模板参数的替换
    • 而替换产生的结果可能没有意义
    • 但替换不会导致错误
    • C++ 语言规则要求忽略这类替换结果
    • 被称为 SFINAE
  • 示例
    //1 number of elements in a raw array:
    template<typename T, unsigned N>
    std::size_t len (T(&)[N])
    {
      return N;
    }
    
    //2 number of elements for a type having size_type:
    template<typename T>
    typename T::size_type len (T const& t)
    {
      return t.size();
    }
    
  • 使用
    int a[10];
    std::cout << len(a); // OK: only len() for array matches
    std::cout << len("tmp"); //OK: only len() for 
    
    • 当传递的参数是原始数组或者字符串字面量时,匹配第一个模板
      • 从签名来看,第二个函数模板参数也可以被替换
      • 但在处理返回类型 T::size_type 时会导致错误
      • 因此第二个函数模板被忽略掉
    • 如果传递 std::vector<>,则只有第二个模板参数能够匹配
    • 传递指针,以上两个模板都不会被匹配
    • 传递 std::allocator<>, 编译器会匹配到第二个函数模板
      • 会报编译期错误, 对 std::allocator<int> 而言 size() 是一个无效调用
      • 模板不会被忽略掉
  • 为了保证任何情况都能成功替换,可以设置备选项
    // number of elements in a raw array:
    template<typename T, unsigned N>
    std::size_t len (T(&)[N])
    {
      return N;
    }
    
    // number of elements for a type having size_type:
    template<typename T>
    typename T::size_type len (T const& t)
    {
      return t.size();
    }
    
    // 如果是其他类型 fallback
    std::size_t len ()
    {
      return 0;
    }
    
    • 通过额外提供了一个通用函数 len(),使得总会匹配调用,但其匹配是所有重载选项中最差的
      • 通过省略号…匹配
  • SFINAE and Overload Resolution
    • SFINAE 掉了一个函数
      • 通过让模板在一些限制条件下产生无效代码,从而略掉该模板
      • 使用 SFINAE 方法 SFINAE 掉了某个函数模板
    • 示例
      namespace std 
      {
        class thread 
        {
        public:template<typename F, typename… Args>
          explicit thread(F&& f, Args&&… args);};
      }
      
      • std::thread 类模板的声明, 并做了备注
        • 如果 decay_t<F> 的类型和 std:thread 相同的话,该构造函数不应该参与重载解析过程
        • 解释
          • 如果在调用该构造函数模板时,使用 std::thread 作为参数,该构造函数模板就会被忽略掉
          • 为了防止拷贝和移动构造函数失效(匹配了构造函数模板)
    • 处理上述问题更好的方案
      • 使用 std::enable_if<>
        namespace std 
        {
          class thread 
          {
          public:template<typename F, 
                    typename… Args,
                    typename = std::enable_if_t<!std::is_same_v<std::decay_t<F>, thread>>>
            explicit thread(F&& f, Args&&… args);};
        }
        

8.4.1 带有 decltype 的 SFINAE 表达式

  • 对于有些限制条件,并不总是很容易设计出合适的表达式来 SFINAE 掉函数模板
  • 示例
    • 对有 size_type 但没有 size() 的参数类型, 想要保证忽略掉函数模板 len(), 如果没有在函数声明中以某种方式要求 size() 成员函数必须存在,则被选中的函数模板在实例化过程中会导致错误
      template<typename T>
      typename T::size_type len (T const& t)
      {
        return t.size();
      }
      
      std::allocator<int> x;
      std::cout << len(x) << '\n'; //ERROR: len() selected, but x has no size()
      
    • 处理上述问题的惯用手法
      • 通过尾置返回类型语法来指定返回类型
      • 使用 decltype 和逗号运算符定义返回类型
      • 将所有需要成立的表达式放在逗号运算符的前面
        • 防止逗号运算符被重载,需要将所有表达式的类型转换为 void
      • 在逗号运算符的末尾定义一个具有真实返回值类型的对象
    • 示例
      template<typename T>
      auto len (T const& t) -> decltype( (void)(t.size()), T::size_type() )
      {
        return t.size();
      }
      

8.5 编译期 if

  • 部分特化、SFINAE 和 std::enable_if 可以一起被用来禁用或启用某个模板
  • C++17 又引入编译期 if 语句也能实现上述的功能
    • 编译器会根据编译期 if 表达式结果来决定是使用 if 中还是 else 对应部分的表达式
  • 示例
    template<typename T, typename… Types>
    void print (T const& firstArg, Types const&… args)
    {
      std::cout << firstArg << ’\n’;
      if constexpr(sizeof(args) > 0) 
      {
        print(args…); //code only available if sizeof…(args)>0 (sinceC++17)
      }
    }
    
  • if constexpr 并不仅可以用于模板函数,还可以用于任意类型的函数

第九章 模板实践

  • 模板介于宏和常规函数声明之间位置

9.1 包含模型 (the include model)

9.1.1 连接器错误

  • 大多数 C 和 C++ 程序员都会按照如下方式组织代码
    • 类和其它类型声明被放在头文件里,文件扩展名为 .hpp(.h, .H, .hh, .hxx)
    • 全局变量(非 inline)和函数(非 inline),将其声明放在头文件里
      • 定义放在一个被当作其自身翻译单元的文件里
      • 文件扩展名为 .cpp(.c, .C, .cc, .cxx)
  • 好处
    • 能在整个程序中很容易的找到所需类型的定义
    • 避免链接过程中的重复定义错误

9.1.2 头文件中的模板

  • 处理模板与处理宏和 inline 函数的方法一样
    • 将模板定义和模板声明都放在头文件里
    • 这种组织模板相关代码的方法被称为 “包含模式”
  • 使用非 inline 函数模板和inline 函数及宏之间有着明显的不同
    • 非 inline 函数模板在被调用的地方不会被展开,而是会被实例化

9.2 模板和 inline

  • 提高程序运行性能的一个常规手段是将函数声明为 inline 函数
  • 编译器可能会忽略 inline 不进行展开
    • 因此,inline 唯一能保证的就是允许函数定义在程序中出现多次
  • 模板调用是否进行 inline 替换由编译器决定
    • 对 inline 函数处理方式受编译器编译选项的影响
    • 通过使用合适的性能检测工具进行测试,可以比编译器更好的决定是否做 inline 展开
      • 可以通过编译器的具体属性控制是否 inline 展开
        • 如 noinline 和 always_inline
  • 函数模板在全特化之后和常规函数是一样的
    • 除非被定义成 inline,否则它只能被定义一次

9.3 预编译头文件

  • 预编译头文件(PCH: precomplied header)
    • 降低编译时间
    • 实现方式由编译器供应商自行决定
  • 利用预编译头文件提高编译速度的关键点
    • 让尽可能多的文件,以尽可能多的相同的代码作为开始
      • 文件要以相同的 #include 指令开始
      • 如果 #include 头文件的顺序相同的话,就会对提高编译速度有很大帮助
  • 示例
    #include <vector>
    #include <list> 
    #include <list>
    #include <vector> 
    • 预编译头文件不会起作用,顺序不一致
  • 创建一个包含所有标准头文件的头文件
  • 推荐组织代码方式:按层级组织
    • 创建包含所有代码稳定的头文件的头文件,称为稳定层
      • 可以重复被使用
    • 创建包含为项目准备但尚未达到稳定状态的头文件的头文件
    • 为稳定层创建的预编译头文件可以被重复使用,以提高不稳定的头文件的编译速度
      #include "std.hpp"
      #include "core_data.hpp
      #include "core_algos.hpp" 

9.4 破译大篇幅错误信息 (略)

9.5 后记(略)

第十章 基本模板术语

  • 本章不适合用中文做笔记

10.1 “类模板” 还是 “模板类”?

  • In C++,structs,classes, and unions are collectively called class type
  • The term class template states that the class is a template
    • it is a parameterized description of a family of classes
  • The term template class, on the other hand, has been used
    • as a synonym for class template
    • to refer to classes generated from templates
    • to refer to classes with a name that is a template-id
  • we avoid the term template class in this book

10.2 替换、实例化和特化

  • template instantiation
    • the process of actually creating a definition for a regular class,type alias,function,member function,or variable from a template by substituting concrete arguments for the template parameters is called template instantiation
  • specialization
    • the entity resulting from an instantiation or an incomplete instantiation is generically called a specialization
  • explicit specialization
    template<typename T1, typename T2> // primary class template
    class MyClass {};
    
    template<> // explicit specialization
    class MyClass<std::string,float> {};
    
  • partial specializations
    • specializations that still have template parameters are called partial specializations
      template<typename T> // partial specialization
      class MyClass<T,T> {};
      
      template<typename T> // partial specialization
      class MyClass<bool,T> {};
      

10.3 声明和定义

  • for a variable, initialization or the absence of an extern specifier causes a declaration to become a definition
    extern int v = 1; // an initializer makes this a definition for v
    int w; // global variable declarations not preceded by extern are alsodefinitions
    

10.3.1 完全和不完全类型

  • types can be complete or incomplete
  • incomplete types are one of the following
    • a class type that has been declared but not yet defined
    • an array type with an unspecified bound
    • an array type with an incomplete element type
    • void
    • an enumeration type as long as the underlying type or the enumeration values are not defined
    • any type above to which const and/or volatile are applied
  • all the other types are complete
    class C;            // C is an incomplete type
    C const* cp;        // cp is a pointer to an incomplete type
    extern C elems[10]; // elems has an incomplete type
    extern int arr[];   // arr has an incomplete type…c
    lass C { };         // C now is a complete type (and therefore cpand elems// no longer refer to an incomplete type)
    int arr[10];        // arr now has a complete type
    

10.4 The One-Definition Rule

  • the C++ language definition places some constraint on the redeclaration of various entities. the totality of these constraints is known as the one-definition rule or ODR
  • it suffices to remember the following ODR basics
    • ordinary noninline functions and member functions,as well as global variables and static data members should be defined only once across the whole program
    • class types,templates and inline functions and variables should be defined at most once per translation unit,and all these definitions should be identical
  • linkable entity refers to any of the following
    • a function or member function,a global variable or a static data member
    • including any such things generated from a template,as visible to the linker

10.5 Template Arguments 和 Template Parameters

  • template-id
    • regardless of whether these arguments are themselves dependent on template parameters,the combination of the template name, followed by the arguments in angle brackets,is called a template-id
  • template arguments and template parameters
    • in short, you can say that “parameters are initialized by arguments”
    • more precisely
      • template parameters are those names that are listed after the keyword template in the template declaration or definition
      • template arguments are the items that are substituted for template parameters
        • unlike template parameters,template arguments can be more than just “names”

第十一章 泛型库

11.1 可调用对象 Callables

  • 函数对象类型(function object types)
    • 函数指针类型
    • 重载了 operator() 的 class 类型 (也称仿函数 functor)
      • 包括 lambda 函数
    • 有可以产生一个函数指针或函数引用的转换函数的 class 类型
  • 函数对象类型的值则称为函数对象(function object)

11.1.1 函数对象的支持

  • 当把函数名当作函数参数传递时,并不是传递函数本身,而是传递其指针或引用
    • 按值传递时,函数参数退化为指针
    • 参数类型是模板参数,类型会被推断为指向函数的指针
  • 显式传递函数指针
    • foreach(primes.begin(), primes.end(), &func);
  • 传递仿函数, 是将一个类的对象当作可调用对象进行传递
    foreach(primes.begin(), primes.end(),
          [] (int i) { 
              std::cout << "lambda called for: " << i << '\n';
          }
    );
    
    • 当调用类对象的 operator()
      • op(*current);
      • 会转换成
      • op.operator()(*current);
    • 对于类对象,可能会被转换为指向代理调用函数( surrogate call function)的指针或引用
      • op(*current);
      • 会转换成
      • (op.operator F())(*current);
        • F 就是类对象所转换成的指向函数的指针或引用的代理类型
  • 在定义 operator() 时最好将其定义成 const 成员函数
  • lambda 表达式会产生仿函数(称闭包)
    • lambda 函数匹配比常规闭包的 operator() 要差

11.1.2 处理成员函数和附加实参

  • 调用一个非静态成员函数
    • object.memfunc(...)
    • ptr->memfunc(...)
  • 与常规函数调用不同
    • func(...)
  • C++17 标准库提供了 std::invlke() 统一函数调用方式
    std::invoke(
                op,      //call passed callable with
                args…,   //any additional args
                *current // and the current element
                ); 
    
  • std::invoke() 处理相关参数方式
    • 如果可调用对象是一个指向成员函数的指针,会将 args... 中的第一个参数当作 this 对象
      • Args... 中其余的参数则被当做常规参数传递给可调用对象
    • 其他类型函数则将所有的参数都被直接传递给可调用对象
  • 注意不能对于可调用对象和 agrs…使用完美转发
    • 因为第一次调用可能会移动(move)相关参数的值,导致在随后的调用中出现错误

11.1.3 包装函数调用

  • std::invoke() 的一个常规用法是包装一个函数调用
    • 记录相关调用时长
    • 准备一些 context
      • 如开启一个线程
    • 此时可以通可调用对象和被传递的参数的完美转发来支持移动语义
      #include <utility> // for std::invoke()
      #include <functional> // for std::forward()
      
      template<typename Callable, typename… Args>
      decltype(auto) call(Callable&& op, Args&&… args)
      {
        return std::invoke(
                          std::forward<Callable>(op), //passed callable with
                          std::forward<Args>(args)// any additional args
                          ); 
      }
      
    • 该如何处理被调用函数的返回值,才能将该返回值 “完美转发” 给调用者
      • 返回引用
        • 需要使用 decltype(auto) 而不是 auto
  • decltype(auto)
    • C++14 引入的占位符
    • 根据相关表达式决定变量、返回值、或模板实参的类型
  • 如果想暂时将 std::invoke()的返回值存储在一个变量中,并在做了某些别的处理后将其返回
    decltype(auto) ret{
              std::invoke(
                        std::forward<Callable>(op),
                        std::forward<Args>(args))
              };return ret;
    
    • 问题
      • 如果可调用对象的返回值是 void,那么将 ret 初始化为 decltype(auto) 是不可行的
        • 因为 void 是不完整类型
    • 解决方法
      • 在当前行前面声明一个对象,并在其析构函数中实现期望的处理行为
        struct cleanup 
        {
          ~cleanup() 
          {//code to perform on return
          }
        } dummy;
        return std::invoke(std::forward<Callable>(op), std::forward<Args>(args));
        
      • 分别实现 void 和非 void 的处理方式
        #include <utility>     // for std::invoke()
        #include <functional>  // for std::forward()
        #include <type_traits> // for std::is_same<> and invoke_result<>
        
        template<typename Callable, typename… Args>
        decltype(auto) call(Callable&& op, Args&&… args)
        {
          if constexpr(std::is_same_v<std::invoke_result_t<Callable, Args…>, void>) {// return type is void:
            std::invoke(std::forward<Callable>(op), std::forward<Args>(args));return;
          }
          else 
          {
            // return type is not void:
            decltype(auto) ret{std::invoke(std::forward<Callable>(op), std::forward<Args>(args))};return ret;
          }
        }
        
  • 后续的 C++ 版本中可能会避免需要这种对 void 进行特殊操作的处理方式

11.2 实现泛型库的其他工具

11.2.1 type traits

  • 用来计算及修改类型
#include <type_traits>

template<typename T>
class C
{
  // ensure that T is not void (ignoring const or volatile):
  static_assert(!std::is_same_v<std::remove_cv_t<T>, void>, "invalid instantiation of class C for void type");
public:
  template<typename V>
  void f(V&& v) 
  {
    if constexpr(std::is_reference_v<T>) 
    { 
      // special code if T is a reference type}
   
    if constexpr(std::is_convertible_v<std::decay_t<V>,T>)
    {
      // special code if V is convertible to T}

    if constexpr(std::has_virtual_destructor_v<V>) 
    { 
      // special code if V has virtual destructor}
  }
}
  • C++17 支持编译期 if 特性
    • 也可以使用 std::enable_if、部分特化或 SFINAE
  • 使用 type trait 需小心,其行为可能与预期不同
    std::remove_const_t<int const&> //yields int const& 
    
    • 由于 & 不是 const 类型,所以上面代码操作不会有任何效果
    • 因此,删除 & 和删除 const 的顺序很重要
      std::remove_const_t<std::remove_reference_t<int const&>> // int
      std::remove_reference_t<std::remove_const_t<int const&>> // int const
      
    • 另一种方法是,直接调用
      std::decay_t<int const&> // yields int
      
      • 但是这会让原始数组和函数类型退化为相应的指针类型
  • 有一些 type trait 的使用是有要求的
    • 如果要求不满足会出现未定义行为
      make_unsigned_t<int> // unsigned int
      make_unsigned_t<int const&> // undefined behavior (hopefully error)
      
  • 还有一些特殊的情况
    add_rvalue_reference_t<int>        // int&&
    add_rvalue_reference_t<int const>  // int const&&
    add_rvalue_reference_t<int const&> // int const& (lvalueref remainslvalue-ref)
    
    • C++ 中的引用折叠(reference-collapsing rules)会令左值引用和右值引用的组合返回一个左值引用
  • 另外一个例子
    is_copy_assignable_v<int> // yields true (generally, you can assignanint to an int)
    is_assignable_v<int, int> // yields false (can’t call 42 = 42)
    
    • is_copy_assignable 检查(检查左值的相关操作)是否能够将一个 int 赋值给另外一个
    • is_assignable 会把值的种类 (value category) 考虑进去,会检查是否能将一个纯右值 (prvalue) 赋值给另外一个
    • 因此,第一个表达式可以改写成
      is_assignable_v<int&,int&> // yields true
      
  • 最后一个例子(处理方式与上一个类似)
    is_swappable_v<int> // yields true (assuming lvalues)
    is_swappable_v<int&,int&> // yields true (equivalent to the previous check)
    is_swappable_with_v<int,int> // yields false (taking value category into account)
    

11.2.2 std::addressof()

  • 函数模板 std::addressof<>() 会返回一个对象或函数的真实地址
    • 即使一个对象重载了运算符 & 也如此
  • 要获取任意类型的对象的地址,推荐使用 addressof()
    template<typename T>
    void f (T&& x)
    {
      auto p = &x; // might fail with overloaded operator &
      auto q = std::addressof(x); // works even with overloaded operator & }
    

11.2.3 std::declval()

  • 函数模板 std::declval() 某一类型的对象引用占位符
    • 没有定义,因此不能被调用(也不能创建对象)
    • 它只能被用作不会被计算的操作数
      • 类似 decltype 和 sizeof
  • 示例
    #include <utility>
    
    template<typename T1, 
            typename T2,
            typename RT = std::decay_t<decltype(true ? std::declval<T1>() : std::declval<T2>())>>
    RT max (T1 a, T2 b)
    {
      return b < a ? a : b;
    }
    
    • 为了避免在调用运算符 ?: 的时候不得不去调用 T1 和 T2 的构造函数,这里使用 std::declval
      • 使上述代码在不创建对象的情况下 “使用” T1 和 T2
      • 只能在不做真正的计算时使用
  • std::declval<> 返回的是右值引用, 需要退化(使用 std::decay_t

11.3 完美转发临时对象

  • 在泛型代码中,有时需转发一些不是通过参数传递进来的数据,这时可以使用 auto && 创建一个可以被转发的临时变量
  • 示例
    • 需相继的调用 get()set() 两个函数,并将 get() 的返回值完美的转发给 set()
      template<typename T>void foo(T x)
      {
      `set(get(x));
      }
      
    • 假设需更新代码, 对 get() 的返回值进行一些操作,可以将 get() 的返回值存储在一个被声明为 auto && 的变量中
      template<typename T>
      void foo(T x)
      {
        auto&& val = get(x);// perfectly forward the return value of get() to set():
        set(std::forward<decltype(val)>(val));
      }
      
    • 这样可以避免拷贝

11.4 引用作为模板参数

  • 模板参数的类型可以是引用类型(不常见)
    #include <iostream>
    
    template<typename T>
    void tmplParamIsReference(T)
    {
      std::cout << "T is reference: " << std::is_reference_v<T> << ’\n’;
    }
    int main()
    {
      std::cout << std::boolalpha;
      int i;
      int& r = i;
      tmplParamIsReference(i); //1 false
      tmplParamIsReference(r); //2 false
      tmplParamIsReference<int&>(i); //3 true
      tmplParamIsReference<int&>(r); //4 true
    }
    
  • 代码 1 和 2 推断的类型永远不可能是引用类型
    • 推断为被引用的类型
  • 可以显示指定 T 的类型为引用类型
    • 如代码 3 和 4
  • 上述做法虽然可以从根本上改变模板的行为,但是由于这并不是模板最初设计的目的,因此可能会发生错误或不可预知的行为
  • 如果尝试用引用对模板进行实例化情况会比较复杂
    • 默认初始化将失效
    • 不能直接用 0 来初始化 int
    • 赋值运算符也不再可用,因为对有非静态引用成员的类,其赋值运算符会被删除
  • 将引用类型用于非类型模板参数也会变的复杂、危险
  • 基于上述原因,C++ 标准库在某些情况下制定了特殊的规则和限制
    • 模板参数被用引用类型实例化,为了能够正常使用赋值运算符,std::pair<>std::tuple<> 都没有使用默认的赋值运算符,而是做了单独的定义
    • C++17 用引用类型实例化标准库模板 std::optional<>std::variant<> 的代码也很奇怪
    • 这两种情况代码在 Page 170

11.5 延迟评估

  • 在实现模板的过程中,有时需面对是否要考虑不完整类型的问题
  • template<typename T>
    class Cont 
    {
    private:
      T* elems;
    public:};
    
    • 该类可以用于不完整类型,这很有用,如
      struct Node
      {
        std::string value;
        Cont<Node> next; // only possible if Cont accepts incomplete types
      };
      
  • 但是某些 type traits,不能用于不完整类型
    • 示例
      template<typename T>
      class Cont 
      {
      private:
        T* elems;
      public:typename std::conditional<std::is_move_constructible<T>::value, T&&,T& >::type foo();
      };
      
    • std::conditional 决定 foo() 的返回值是 T&& 还是 T& 类型
      • 条件为模板参数 T 是否支持移动语义
    • 但是 std::is_move_constructible 需要参数 T 是完整类型
    • 因此需要延迟 std::is_move_constructible 计算
      • 使用一个成员模板代替 foo() 的定义,可以将 std::is_move_constructible 的计算推迟到 foo() 的实例化阶段
        template<typename T>
        class Cont 
        {
        private:
          T* elems;
        public:
          template<typename D = T>
          typename std::conditional<std::is_move_constructible<D>::value, T&&,T&>::type foo();
        };
        

11.6 编写泛型库时需要考虑的事情

  • 建议
    • 在模板中使用转发引用来转发参数, 如果参数不依赖于模板参数,就使用 auto &&
    • 当参数声明为转发引用时,请确保模板参数在传递左值时具有引用类型
    • 在需要一个依赖于模板参数的对象地址时,最好使用 std::addressof(),这样能避免因为对象重载 operator & 导致的意外情况
    • 对于成员函数模板,需要确保它们不会比预定义的拷贝/移动构造函数或赋值运算符更匹配某个调用
    • 如果模板参数可能是字符串字面量,并且不是按值传递,考虑使用 std::decay
    • 如果模板参数列表有 out 或 inout 类型参数,请为 const 类型的模板参数指定相应规则
    • 为将引用用于模板参数制定规则。尤其是要确保返回值类型不是引用的情况
    • 为将不完整类型用于嵌套数据结构中这类情况制定规则
    • 为所有数组类型进行重载,不要使用只是 T[SZ]
  • 上述建议的示例在书中都能找到,而且书中这些建议每条都有标注示例章节
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值