Reading Note of CppPrimer-Chapter16

Template and Generic Programming

Template Definition

  • 模版的形式

    模版的定义以关键字 template 开始,后跟一个模版参数列表(template parameters list),这是一个逗号 , 分隔的一个或者多个模版参数(template parameters) 的列表,用小于号 < 和大于号 > 括起来

  • 模版的参数

    • 模版参数列表很像函数参数列表,定义了若干特定类型的局部变量但并未指明如何初始化他们;在运行时,由调用者初始化形参

    • 在模版的定义中,模版的参数列表不能为空;注意区别于模版特例化

    • 模版的参数分为类型参数和非类型参数

    • 模版参数遵循作用域规则,可以隐藏外层作用域的同名变量,但是不能在模板内重用模板参数名

      1
      2
      3
      4
      5
      template <typename A, typename B>
      void f(A a, B b){
      A tmp = a;
      double B= 0; // invalid, can not reuse template parameter name
      }
  • 模版类型参数

    • 一般来说,可以将类型参数看作是类型说明符,就像内置类型或者类类型说明符

    • 在类型参数前需要使用关键字 typename 或者 class,在模版参数列表中,这两个关键字的含义相同,甚至可以混合使用

    • 类型参数可以用来指定函数的返回值类型或者函数参数类型,以及用于变量声明或者类型转换

      1
      2
      3
      4
      5
      template <typename T>
      T foo(T* p){ // return value type, function parameter type
      T tmp = *p; // define variable
      return tmp;
      }
    • 可以用 void 作为类型参数的默认参数

      1
      2
      template <typename T0, typename T1 = void, typename T2 = void> 
      class Tuple;
    • 模板的默认参数不仅仅可以是一个确定的类型,它还能是以其他类型为参数的一个类型表达式

      在实例化的时候,尽管我们只为 SafeDivide 指定了参数 T,但是它的另一个参数 IsFloat 在缺省的情况下,可以根据 T 求出表达式 std::is_floating_point<T>::value 的值作为实参的值,带入到 SafeDivide 的匹配中。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      template <typename T, bool IsFloat = std::is_floating_point<T>::value>
      struct SafeDivide {
      static T Do(T lhs, T rhs) {
      return CustomDiv(lhs, rhs);
      }
      };
      template <typename T>
      struct SafeDivide<T, true>{ // 偏特化
      static T Do(T lhs, T rhs){
      return lhs/rhs;
      }
      };
      template <typename T>
      struct SafeDivide<T, false>{ // 偏特化
      static T Do(T lhs, T rhs){
      return lhs;
      }
      };
  • 模版非类型参数

    • 一个非类型参数表示一个值而并非是一个类型,通过特定的类型名而不是关键字class 或 typename 来声明

      • 在 C++20 之前,非类型参数只可以是整形、指向对象或者函数类型的指针或者左值引用

        1
        2
        3
        4
        template <float SCORE>
        float get_score(){
        return SCORE;
        }
        1
        2
        error: a non-type template parameter cannot have type 'float' before C++20
        template <float SCORE>
    • 当一个非类型参数模版被实例化时,非类型参数被一个用户提供或者编译器推断出的值所代替

      • 用户提供

        1
        std::array<int, 3>;
      • 编译器推断(编译器可以为函数模版推断参数,但是不可以给类模版推倒 (until C++17),使用类模版必须在模版名后的尖括号内提供多余信息)

        1
        2
        3
        4
        5
        6
        7
        8
        template<unsigned int N, unsigned int M>
        int compare(const char (&p1)[N], const char (&p2)[M]){
        return strcmp(p1, p2);
        }

        compare("hi", "mom"); // compile deduced that N=sizeof("hi")=3, M=sizeof("mom")=4
        // equal to user specified parameters
        compare<3, 4>("hi", "mom");
    • 绑定到非参数类型的整形参数必须是一个常量表达式,绑定到非参数类型的指针或者引用类型必须具有静态生存期

    • 在模版定义内,模版非类型参数是一个常量值 (constexpr),可以用在任何需要使用常量值的地方

  • 访问模板参数的类型成员(typename)

    一般的类在定义完整的情况下,通过作用域访问一个类成员,是可以知道这个成员时类型还是数据成员;但是模板类不可以,因为在未实例化的时候,编译器得不到更详细的信息

    默认情况下,C++ 语言假定通过域作用符访问的名字不是类型

    如果需要使用模板参数的类型成员,需要显式的告诉编译器该变量是一个类型,可以通过 typename 实现而不能用 class

    1
    2
    3
    4
    5
    6
    7
    8
    9
    template <typename T>
    typename T::value_type top(const T& c){ // return T::value_type, specified by typename, indicate
    if (!c.empty()){ // it is a type value rather than data member
    return c.back();
    }
    else{
    return typename T::value_type(); // specified by typename
    }
    }

    事实上,标准对 typename 的使用规定极为复杂,也算是整个模板中的难点之一。如果想了解所有的标准,需要阅读标准 14.6 节下 2-7 条,以及 14.6.2.1 第一条中对于 current instantiation 的解释。

    简单来说,如果编译器能在出现的时候知道它的类型,那么就不需要 typename,如果必须要到实例化的时候才能知道它是不是合法,那么定义的时候就把这个名称作为变量而不是类型。

    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
    struct A;
    template <typename T> struct B;
    template <typename T> struct X {
    typedef X<T> _A; // 编译器当然知道 X<T> 是一个类型。
    typedef X _B; // X 等价于 X<T> 的缩写
    typedef T _C; // T 不是一个类型还玩毛

    // !!!注意我要变形了!!!
    class Y {
    typedef X<T> _D; // X 的内部,既然外部高枕无忧,内部更不用说了
    typedef X<T>::Y _E; // 嗯,这里也没问题,编译器知道Y就是当前的类型,
    // 这里在VS2015上会有错,需要添加 typename,
    // Clang 上顺利通过。
    typedef typename X<T*>::Y _F; // 这个居然要加 typename!
    // 因为,X<T*>和X<T>不一样哦,
    // 它可能会在实例化的时候被别的偏特化给抢过去实现了。
    };

    typedef A _G; // 嗯,没问题,A在外面声明啦
    typedef B<T> _H; // B<T>也是一个类型
    typedef typename B<T>::type _I; // 嗯,因为不知道B<T>::type的信息,
    // 所以需要typename
    typedef B<int>::type _J; // B<int> 不依赖模板参数,
    // 所以编译器直接就实例化(instantiate)了
    // 但是这个时候,B并没有被实现,所以就出错了
    };
  • 模版的默认参数

    新标准中可以为函数模板和类模板提供默认参数:比如 std::vector 的模板参数 allocator 就有默认值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    template <typename T, typename F=std::less<T>>
    int compare(const T& v1, const T& v2, F f=F()){
    if (f(v1, v2)) return -1;
    if (f(v2, v2)) return 1;
    return 0;
    }
    static int compare_int(const int& a, const int& b){
    if (a < b) return -1;
    if (b < a) return 1;
    return 0;
    }
    auto func = [](const int& a, const int& b){
    if (a < b) return -1;
    if (b < a) return 1;
    return 0;
    };

    // use default parameter
    auto v = compare(1.0, 1.0);
    // pass third parameter explicitly
    auto v1 = compare(1, 1, compare_int);
    auto v2 = compare(1, 2, func);
  • 函数模版

    一个函数模版就像是一个公式,可以用来生成针对特定类型的函数版本

    函数模版是可以声明成为 inline 和 constexpr 的,如同普通函数一样,这些关键字出现在参数列表之后

    编译器可以为函数模版推断参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    template <typename T>
    int compare(const T& v1, const T& v2){
    if (std::less<T>(v1, v2)){
    return -1;
    }
    if (std::less<T>(v2, v2)){
    return 1;
    }
    return 0;
    }

    compare(1.1, 2.2); // auto deduced to T is double
    compare(1L, 1L); // auto deduced to T is long int
  • 类模版:

    类模版是用来生成一系列类的;类模版的名字不是类型名,类模版是用来实例化类型,而一个实例化的类型总是包含模版参数的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    template <typename T>
    class Blob{
    public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;

    Blob()){std::cout << "Ctor of BlobA" << std::endl;}
    Blob(std::initializer_list<T> il);

    // inside class scope, Blob equal to Blob<T>
    //Blob<T>(){std::cout << "Ctor of BlobA" << std::endl;}
    //Blob<T>(std::initializer_list<T> il);

    size_type size() const {return data->size();}
    bool empty() const{return data->empty();}
    T& operator[](size_type i);
    T& back();
    private:
    std::shared_ptr<std::vector<T>> data;
    void check(size_type i, const std::string&msg) const;
    };

    Blob<int> blob;

    为了使用类模版,必须在模版名后的尖括号内提供多余信息,即模版实参列表 (until C++17)

    1
    2
    Blob<int> int_b;
    Blob<float> float_b;

    与其他类相同,我们可以在类模版的内部或者类模版的外部为其定义成员函数,定义在内部的成员函数默认 inline

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template <typename T>
    Blob<T>::Blob(){
    // init ctor
    }

    template <typename T>
    void Blob<T>::check(size_type i, const std::string& msg) const{
    if (i >= data->size()){
    throw std::out_of_range(msg);
    }
    }

    默认情况下,一个类模版的成员只有当程序用到它的时候才进行实例化;如果一个成员没有被使用,则不会实例化

    1
    2
    3
    4
    5
    // only instance Blob<int> and 3 corresponding function members: ctor, size, operator[]
    Blob<int> squares = {0,1,2,3,4,5,6}; // call Blob(std::initializer_list<T> il);
    for (size_t i=0; i<squares.size(); i++){ // call size_type size() const;
    squares[i] i*i; // call T& operator[](size_type i);
    }

    在模版类的作用域内,可以简化模版类名的使用,我们可以直接使用模版名而不需要加模版参数;但是在类的作用域外,还是必须老老实实模版名<参数名>,直到遇到类名,类名之后才进入类的作用域

    1
    2
    3
    4
    5
    6
    template <typename T>
    BlobPtr<T> BlobPtr<T>::operator++(int){
    BlobPtr ret = *this;
    ++*this;
    return ret;
    }

    类模版的别名:虽然没法 typedef 引用一个模版 Blob<T>,但是新标准允许我们使用 using 为类模版指定别名

    1
    2
    3
    4
    5
    6
    7
    template <typename T> using Pair = std::pair<T, T>;
    Pair<int> i_twin; // equal to std::pair<int, int>
    Pair<double> d_twin; // equal to std::pair<double, double>

    template <typename T> using UnsignedPair = std::pair<T, unsigned int>;
    UnsignedPair<int> a; // equal to std::pair<int, unsigned int>;
    UnsignedPair<float> a; // equal to std::pair<float, unsigned int>;
  • 类模版的静态成员

    • 每个类模版的实例都有自己 static 成员类型,所有的类模版的实例的对象们共享一个静态变量

      1
      2
      3
      4
      5
      6
      7
      template <typename T> 
      struct Foo{
      static int val;
      };

      Foo<std::string> fs1, fs2; // share same static member
      Foo<int> fi1, fi2, fi3; // share same static member but different from fs1'
    • 每个静态数据成员都需要在类外进行定义,而且有且仅有一个定义;与定义模版的成员函数类似,将 static 数据成员也定义为模版

      1
      2
      3
      // this is not a function or class template
      template <typename T>
      int Foo::val = 0;
    • 与非模版类的静态成员类似,我们可以通过类类型的对象和域访问符来访问类模版的静态变量

      1
      2
      3
      Foo<int> fi;
      fi.val = 10;
      Foo<int>::val = 20;
  • 类模版与友元

    当一个类包含一个友元声明,类和友元各自是否是模版是不相关的

    • 一对一 (one-to-one) 友元关系:即类模版的特定实例与其友元的关系

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      // one-to-one
      template <typename T> class BlobStr;
      template <typename T> class Blob;
      template <typename T>
      bool operator==(const Blob<T>& lhs, const Blob<T>& rhs){
      std::cout << "equal: " << (lhs.val == rhs.val) << std::endl;
      return true;
      }
      // template parameter is T
      template <typename T> class Blob{
      // 每个Blob实例将访问权限授予用相同类型实例化的operator==运算符
      // template parameter is also T
      friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
      int val = 0;
      };
    • 一对多 (one-to-many) 友元关系:即一个类可以与另一个类模版的任何实例构成友元关系

      1
      2
      3
      4
      5
      // one-to-many
      template <typename T> class Pal;
      class C {
      friend class Pal<C>; // 用C实例化的Pal是C的友元
      };
    • 多对多 (many-to-many) 友元关系:即一个类模版的任何实例可以与另一个类模版的任何实例构成友元关系

      1
      2
      3
      4
      5
      // many-to-many
      template <typename T> class C2{ // template parameter is T, different from X
      template <typename X> friend class Pal2; // Pal2的所有实例都是C2的每个实例的友元,
      // 这种情况无需前置声明Pal2
      };
    • 可以令模版自己的类型参数为友元

      1
      2
      3
      4
      template <typename T> 
      class Bar{
      friend T;
      };
  • 成员模板 (member template)

    利用函数模板可以自动推导模板的类型,一个类(包含模板类)可以包含本身是模板的成员函数,这种成员叫做成员模板

    • 普通类的成员模板

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      class DebugDeleter{
      public:
      DebugDeleter()=default;
      template <typename T> void operator(T* p){delete p;} // member template
      };

      // instance
      auto deleter = DebugDeleter();
      double *p = new double;
      deleter(p); // call void operator()(double *);
      int *i = new int;
      deleter(i); // call void operator()(int *);

      // call void operator()(std::string)
      std::unique_ptr<std::string, DebugDelete>(new std::string, DebugDelete());
    • 模版类的成员模版

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      template <typename T>
      class Blob {
      template <typename It> Blob(It begin, It end){
      // copy
      }
      };

      // define outside class template
      template <typename T>
      template <typename It>
      Blob<T>::Blob(It begin, It end){}

      // instance
      int a1[] = {1,2,3,4,5,6};
      std::vector<long> a2 = {0,1,2,3,4,5};
      // call Blob<int>(int* begin, int* end);
      Blob<int> b(a1, a1+5);
      // calll Blob<long>(std::vector<long>::iterator, std::vector<long>::iterator)
      Blob<long> c(a2.begin(), a2.end());

Template Compilation

  • 模板不支持分离式编译

    如果分离编译会发生什么?

    1
    2
    3
    4
    5
     >// test.h
    #include <iostream>
    using namespace std;
    template<class T1, class T2>
    void func(T1 t1, T2 t2);
    1
    2
    3
    4
    5
    6
    7
     >// test.cpp
    #include "test.h"
    using namespace std;
    template<class T1, class T2>
    void func(T1 t1, T2 t2) {
    cout<<t1<<t2<<endl;
    }
    1
    2
    3
    4
    5
    6
    7
     >// main.cpp
    #include "test.h"
    int main() {
    string str("downey");
    int i=5;
    func(str,5);
    }

    标准的分离编译模式:在 test.h 中声明 func(), 在 test.cpp 中定义 func() 实现,在 main.cpp 中包含 test.h 头文件并应用 func()。编译运行会报错,可以见得分离编译对于模版是行不通的。

    1
    >g++ test.cpp test.h main.cpp -o test
    1
    2
    3
     >/tmp/ccqLWRwf.o: In function `main':
    main.cpp:(.text+0x6c): undefined reference to `void func<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int)'
    collect2: error: ld returned 1 exit status

    reference:https://blog.csdn.net/c_base_jin/article/details/72861789

  • 模版的编译

    模板只有在实例化的时候才会生成代码,一般编译模版分三步:

    • 第一阶段是编译模板本身。编译器没有太多可以检查,一般会做语法检查,比如忘记分号、变量名不存在等错误;

    • 第二阶段是遇到模板的声明时,编译器也没有很多可以检查的,一般检查参数数目、类型是不是正确等;

    • 第三阶段是模板实例化时,这个时候编译器才正式生成代码,发现类型相关错误,以及最后链接时可能出的错误;

    如果只用 test.cpp 和 test.h 文件来编译出目标文件会发生什么?

    首先,这种方式是可行的,用 g++ 提供的 -C 参数对目标文件只编译不链接:

    1
    g++ -C test.cpp test.h -o test.o

    生成的 test.o 文件,用 linux 下的 nm 命令查看目标文件符号表:

    1
    nm -C -n test.o
    1
    2
    3
    4
    5
    6
    7
    U __cxa_atexit
    U __dso_handle
    U std::ios_base::Init::Init()
    U std::ios_base::Init::~Init()
    0000000000000000 t __static_initialization_and_destruction_0(int, int)
    0000000000000000 b std::__ioinit
    000000000000003e t _GLOBAL__sub_I_test.cpp

    丝毫看不到 func() 的影子,也就是说编译出的二进制文件中根本没有为 func() 分配内存空间,这是为什么呢?

    答案是:模板只有在使用的时候才进行实例化

    通俗地说,用户写一个模板定义,模板本身提供任何数据类型的支持,而编译器在编译出的目标文件中只支持确定的类型例如 func(int,int),func(string char),而不能支持 func(模板类,模板类...)(注意模板是 C++ 的特性而非编译器的特性)。

    编译器在编译的时候根本不知道用户要传入什么样的参数,所以无法确定模板的实例,所以编译器只能等到用户使用此模板的时候才能进行实例化,才能确定模板的具体类型,从而为其分配内存空间,像上述例子中,模板没有实例化,编译器也就不会为模板分配内存空间。

    为什么同时编译 test.cpp`` test.h main.cpp 的例子中,模板在 main 函数中有进行实例化,还是会提示无定义行为呢?

    事实上编译器是对所有的.cpp 文件分开编译的:

    main.cpp 依赖 test.h, 就会将 main.cpp 和 test.h 编译成 main.o 目标文件

    test.cpp 依赖 test.h,将 test.cpp 和 test.h 编译成 test.o 目标文件

    然后链接器将 main.o 和 test.o 以及一些标准库链接成可执行文件。

    但是由上的分析得知,在编译 test.o 文件过程中编译器并没有将 func() 实例化,所以 test.o 中也就是没有 func() 的定义,所以在链接的时候出现未定义行为。

    如果在.cpp 文件中将模板实例化呢?

    在以上的示例中,我在 test.cpp 中添加一个 func() 的调用,main.cpp 保持不变:

    1
    2
    3
    void func1(string str,int val) {
    func(str,val); //将func实例化
    }

    这时候再进行编译竟然可以编译通过,而且键入命令可以正常运行,这个例子说明只要我在定义文件中进行了实例化,编译器就会为这个模板分配内存空间。

    1
    g++ test.cpp test.h main.cpp -o test
    1
    ./test

    reference:https://blog.csdn.net/c_base_jin/article/details/72861789

  • 解决模板这种编译上的特性导致的问题

    • 解决方案 1:提前实例化模板

    一种分离编译的解决方案,在模板定义文件中将所有要用到的模板函数都进行实例化,不过说实话,这很扯蛋,而且完全不符合程序的美学。设计泛型接口本身就是为了多态,并不需要知道调用者以什么方式调用,这样实现的话接口设计者就得知道调用者的所有调用方式!但事实上是确实可以这么做。

    • 解决方案 2:定义实现全部放在同一个.h 头文件中

    为什么这样又可以呢?当某个.cpp 文件用到某个模板时,包含了相应的头文件,而头文件中同时由模板的定义和声明,在.cpp 文件中使用就相当于对这个模板进行了实例化 (.cpp 文件依赖.h 文件编译成.o 文件,.cpp 文件中实例化,.h 文件中进行定义和声明),这样就可以使用模板了。

    这也是常见的做法,STL 就是这样实现的,遗憾的是,这违背了分离编译的思想。

    reference:https://blog.csdn.net/c_base_jin/article/details/72861789

  • smart 技术

    由于在模板实例化出现的地方都会导致生成实例化类的定义代码,这样不同的源文件用了多少模板的实例化,就会生成多少份实例化的定义,这也意味着多个独立编译的源文件很可能使用了相同的定义代码。如果不对这些链接去重,在链接的时候一定会出现重复定义 (duplicate symbols) 的问题;

    不同的编译器,其对模板的编译和链接技术也会有不同,其中一个常用的技术称之为 smart,其基本原理如下:

    1. 模板编译时,以每个 cpp 文件为编译单位,实例化该文件中的函数模板和类模板
    2. 链接器在链接每个目标文件时,会检测是否存在相同的实例;有存在相同的实例版本,则删除一个重复的实例,保证模板实例化没有重复存在。

    比如我们有一个程序,包含 A.cpp 和 B.cpp,它们都调用了 CThree 模板类,在 A.cpp 中定义了 int 和 double 型的模板类,在 B.cpp 中定义了 int 和 double 型的模板类;在编译器编译时.cpp 文件为编译基础,生成 A.o 和 B.o 目标文件,即使 A.o 和 B.o 存在重复的实例版本,但是在链接时,链接器会把所有冗余的模板实例代码删除,保证 exec 中的实例都是唯一的。

    reference:https://blog.csdn.net/c_base_jin/article/details/72861789

  • 进一步控制模板的实例化

    在一些大系统中,对多个文件中实例化相同的模版的额外开销可能非常大,相当于每个实例化都要编译一遍

    新标准中,可以通过显式实例化 (explicit instantiation) 规避这种开销

    • 当编译器遇到 extren template 的声明时,不会在本文件 (编译单元,compile unit) 生成实例化代码
    • extern 声明必须出现在任何使用此实例化代码之前,否则编译器会自动对其实例化
    • 对于每个实例化声明,程序的某个位置一定要有其显示的实例化定义
    1
    2
    3
    // declaration 是一个类或者函数的声明,即模板的实例化,模板的参数都替换位了模板实参
    extern template declaration; // 显式实例化声明
    template declaration; // 显式实例化定义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // application.cc
    extern template class Blob<std::string>;
    extern template class Blob<int>;
    extern template int compare(const int&, const int&);

    Blob<std::string> sa1, sa2; // not generate code
    Blob<int> a = {0,1,2,3,4,5,6}; // not generate code
    int i = compare(a[0], a[1]); // not generate code

    // template_builder.cc
    template class Blob<std::string>; // explicitly instance template and generate code
    template class Blob<int>;
    template int compare(const int&, const int&);

Template Argument Deduction

  • 模板参数类型推断支持的类型转换

    • 顶层 const 无论在实参还是形参中,都会被忽略

      1
      2
      3
      4
      5
      template <typename T> T fobj(T, T);

      std::string s1("hello");
      const std::string s2("hi");
      fobj(s1, s2); // 顶层const被忽略
    • 可以将一个非底层 const 的引用 (或指针) 的实参传递给一个底层 const 的引用 (或指针) 的形参

      1
      2
      3
      4
      5
      template <typename const T&> T fref(const T&, const T&);

      std::string s1("hello");
      const std::string s2("hi");
      fref(s1, s2); // 可以将s1转换为const
    • 如果函数的形参有数组或者函数且都不是引用类型,可以对数组或者函数的实参应用正常的指针类型转换;数组转换为指向其首元素的指针,函数转换为函数类型的指针

      1
      2
      3
      int a[10], b[40];
      fobj(a, b); // valid, convert array to pointer int*
      fref(a, b); // invalid, a b have different array size, so the type diffs
    • 其他的所谓隐式的类型转换在模版参数推导这里都不支持

      • 算术转换
      • 派生类到基类的转换
      • 用户定义的转换,比如构造函数 implicit 的转换、类的类型限定符等
      1
      2
      3
      4
      5
      template <typename T, typename F=std::less<T>>
      int compareF(const T& v1, const T& v2, F f=F());

      long lng = 1;
      compare(lng, 1); // invalid, compare(long, int), algorithm conversion not supported
    • 如果函数参数类型不是模版参数,则在函数匹配的时候对实参进行正常的类型转换

      1
      2
      3
      4
      5
      6
      template<typename T> 
      ostream& print(ostream &os, const T& obj);

      print(std::cout, 42); // instance print(ostream&, int);
      ofstream f("output.txt");
      print(f, 42); // convert ofstream to ostream implicitly
  • 函数模版显式实参

    某些情况下,编译器无法推断出函数实参的类型;另外一些情况下,我们希望允许用户控制模版实例化;特别是在函数返回值的类型和模版参数类型都不同的时候最为有用

    • 显式模版实参按照从左到右的顺序与对应的模板参数匹配

    • 显式指定的实参可以正常隐式类型转换

      1
      2
      3
      4
      5
      6
      template <typename T1, typename T2, typename T3>
      T1 sum(T2, T3);

      long lng = 10;
      sum<long long>(12, lng); // instance long long sum(int, long);
      sum<long, float>(12, lng); // instance long sum(float, long);
  • 尾置返回类型

    • 比如模板参数是迭代器类型,此时并不知道模板函数返回的准确类型,但知道所需类型是所处理的序列的元素类型;定义此函数,如果直接把返回类型作为模板参数传进去也不是不行,但是会给用户带来负担,此时使用尾置返回就很方便

      1
      2
      3
      4
      5
      template <typename It>
      auto fcn(It beg, It end) -> decltype<*beg>{
      // do something
      return *beg;
      }
    • 有时候无法直接获取所需要的类型,比如 decltype 返回的是引用,但是我们希望返回类型的值;此时可以使用标准库的类型转换 (type transformation)

      这些类型转换的模版的工作方式都一样,每个模板都有一个名为 type 的 public 成员,表示一个类型

      • std::remove_reference, std::add_lvalue_reference, std::add_rvalue_reference,

        std::add_pointer, std::remove_pointer

      • std::add_const

      • std::make_signed, std::make_unsigned

      1
      2
      3
      4
      5
      6
      7
      #include <type_trait>

      template <typename It>
      auto fcn(It beg, It end) -> typename std::remove_reference<decltype<*beg>>::type{
      // do something
      return *beg;
      }
    • std::remove_reference 的 STL 实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      /// remove_reference: gcc/libstdc++-v3/include/std/type_traits
      template<typename _Tp>
      struct remove_reference
      { typedef _Tp type; };

      template<typename _Tp>
      struct remove_reference<_Tp&>
      { typedef _Tp type; };

      template<typename _Tp>
      struct remove_reference<_Tp&&>
      { typedef _Tp type; };
  • 使用函数模板初始化函数指针的类型推断

    • 可以使用一个函数模板来初始化或者赋值一个函数指针,编译器通过函数指针的参数类型来推断模板实参类型

      1
      2
      3
      4
      template <typename T> 
      int compare(const T&, const T&);

      int (*fp1)(const int&, const int&) = compare; // instance int compare(const int&, const int&);
    • 当函数模版当作函数的函数指针类型的参数的时候,程序上下文需确保模板参数能唯一确定其类型

      1
      2
      3
      4
      void func(int(*)(const std::string&, const std::string*));
      void func(int(*)(const int&, const int&));

      func(compare); // invalid, compile can not determinate whether T is std::string or int

      此时可以显式指定参数类型:

      1
      2
      func(compare<int>);                // valid
      func(compare<std::string>); // valid
  • 左值引用形参的实参推断

    • 普通左值引用 (lvalue reference)

      可以传递给形参一个左值;如果实参有 const,则实参被推断为 const;也可以传递右值引用 (引用折叠)

      1
      2
      3
      4
      5
      6
      7
      template <typename T> void f1(T&);

      int i = 10;
      const int ci = 15;
      f1(i); // T is int
      f1(ci); // T is const int
      f1(5); // error, can not bind a rvalue to a non-const lvalue-reference
    • 常量左值引用 (const lvalue-reference)

      可以传递任何参数(任意左值、右值);const 已经是形参的一部分,所以实参类型不会被推断为 const

      1
      2
      3
      4
      5
      6
      7
      template <typename T> void f2(const T&);

      int i = 10;
      const int ci = 15;
      f2(i); // T is int
      f2(ci); // T is int, not const int, because parameter is const already
      f2(5); // T is int, pass a rvalue to const lvalue-reference
  • 右值引用形参的实参推断

    正常绑定规则告诉我们只可以传递一个右值给右值引用形参,推导过程类似普通左值引用

    1
    2
    3
    template <typename T> void f3(T&&);

    f3(42); // T is int

    通常而言,是不可以将左值绑定给右值引用,但是 C++ 语言在正常的绑定规则之外定义了两个例外:

    第一:C++ 规定可以将左值类型的实参绑定到一个模版的右值引用类型(T&&)的形参上

    1
    2
    3
    4
    5
    6
    7
    template <typename T> void f3(T&&);

    int i = 10;
    const int ci = 15;
    f3(42); // T is int&, similar to void f3<int&>(int& &&), via reference folding,
    // convert to int&, argument like a rvalue-reference to a lvalue-reference
    f3(ci); // T is const int&

    第二: 引用折叠 (Reference Collapsing):我们可以间接创建了一个引用的引用,此时这些引用会发生引用折叠

    • 通常我们不能直接定义一个引用的引用,但是 可以通过类型别名 (typedef, using) 以及模版参数间接定义

    • 引用折叠只能应用于间接创建的引用的引用,比如类型别名和模版参数 ; 参数折叠不一定都发生在模版函数的参数

      1
      2
      3
      4
      5
      6
      7
      8
      #include <type_trait>
      template <typename T>
      void test_func(T &&){
      std::cout << "int& && is lvalue: " << std::is_lvalue_reference<T&&>::value << std::endl;
      }

      int i = 10;
      test_func(i); // int& && is lvalue: 1, reference folding occurs
    • 折叠规则:

      右值引用的右值引用折叠为右值引用X&& && -> X&&

      其他的引用的引用都折叠为左值引用X& && -> X&, X&& & -> X&, X& & -> 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
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      /// code to validate reference folding 

      /// integral_constant
      template<typename _Tp, _Tp __v>
      struct integral_constant {
      static constexpr _Tp value = __v;
      typedef _Tp value_type;
      };

      /// The type used as a compile-time boolean with true value.
      using true_type = integral_constant<bool, true>;
      /// The type used as a compile-time boolean with false value.
      using false_type = integral_constant<bool, false>;

      /// is_lvalue_reference
      template<typename>
      struct is_lvalue_reference
      : public false_type { };

      template<typename _Tp>
      struct is_lvalue_reference<_Tp&>
      : public true_type { };

      typedef int& int_l_ref;
      typedef int&& int_r_ref;
      typedef int_l_ref&& int_l_r_ref;
      typedef int_r_ref&& int_r_r_ref;
      using int_l_l_ref = int_l_ref&;
      using int_r_l_ref = int_r_ref&;

      is_lvalue_reference<int_l_ref>::value; // 1: lvalue
      is_lvalue_reference<int_r_ref>::value; // 0: rvalue
      is_lvalue_reference<int_l_r_ref>::value; // 1
      is_lvalue_reference<int_r_r_ref>::value; // 0
      is_lvalue_reference<int_l_l_ref>::value; // 1
      is_lvalue_reference<int_r_l_ref>::value; // 1
    • 这两个例外导致了两个重要结果:

      • 1,如果一个函数的参数是指向模版类型参数的右值引用 T&&,则他可以绑定到一个左值,而且
      • 2,如果实参是一个左值,则推断出的模版参数类型为一个左值引用 T&,且模板参数被实例化为左值引用 T&
    • 结论:如果一个函数参数是指向模板参数类型的右值引用 T&&,则可以传递给他任意类型的实参

      • 如果传递的是右值,则实参的自动推导和普通左值引用的类似
      • 如果传递的是左值引用,则引用折叠为左值引用
      • 如果传递的是右值引用,则引用折叠为右值引用
    • 右值引用作为模板参数通常用于两个地方:

      • 模板转发其实参
      • 模板被重载
    • std::move:模板参数为右值引用的实例,通过引用折叠,可以与任何类型的实参匹配

      可以显式的用 static_cast 将一个左值转换为右值引用,这样的特性可以允许截断左值(clobber the lvalue)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // std::move STL source code: gcc/libstdc++-v3/include/bits/move.h

      /**
      * @brief Convert a value to an rvalue.
      * @param __t A thing of arbitrary type.
      * @return The parameter cast to an rvalue-reference to allow moving it.
      */
      template<typename _Tp>
      _GLIBCXX_NODISCARD
      constexpr typename std::remove_reference<_Tp>::type&&
      move(_Tp&& __t) noexcept
      { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

      regardless pf argument is lvalue reference or rvalue reference, std::move return rvalue reference

  • 转发 (forward)

    将模板的一个或者多个实参连同类型不变地转发给模板内的其他函数

    • 问题的出现:模板参数传值调用,但是内层函数传引用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      template<typename F, typename T1, typename T2>
      void flip1(F f, T1 t1, T2 t2){
      f(t1, t2);
      }
      void f1(int v1, int v2){std::cout << v1 << " " << ++v2 << std::endl;}
      void f2(int v1, int& v2){std::cout << v1 << " " << ++v2 << std::endl;}

      int val2 = 1;
      flip1(f1, 1, val2); // valid
      flip1(f2, 1, val2); // not as expected, value of val2 did not change because flip pass val2 as value
      // the template is instantated as void flip1(void(*)(int,int), int, int);
    • 解决方案 1: 使用右值引用作为模板参数

      如果一个函数参数是指向模板类型参数的右值引用 (T&&),则他对应的实参的 const 属性以及左值 / 右值属性可以得到保留

      • 使用引用可以保留 const 属性:因为引用的 const 是底层的,类型推导的时候会携带 const 属性
      • 使用右值引用可以保存左值右值属性:因为引用折叠
      1
      2
      3
      4
      5
      6
      7
      8
      template<typename F, typename T1, typename T2>
      void flip2(F f, T1&& t1, T2&& t2){
      f(t1, t2);
      }

      int val2 = 1;
      int&& val2_rf = std::move(val2);
      flip2(f2, 1, val2); // as expected, because T2 is deduced as int&(int& && -> int&)

      这个版本的 flip2 函数解决了一半问题,对于接受左值引用的函数工作得很好,但是不能用于接受右值引用作为参数的函数

    • 假如 f 的某个参数为右值引用:

      1
      2
      void f3(int v1, int&& v2){std::cout << v1 << " " << ++v2 << std::endl;}
      flip2(f3, 1, 1);

      编译不通过:因为函数的参数都是左值,此时 f3 只是一个普通函数,所以不能通过引用折叠将一个左值引用绑定给右值引用,将 flip2 的 t2 参数(左值)传给 f3 的 val2 参数(右值)不合法

      1
      2
      3
      4
      5
      d8.cpp:11:10: error: rvalue reference to type 'int' cannot bind to lvalue of type 'int'
      f(t1, t2);
      ^~
      d8.cpp:41:5: note: in instantiation of function template specialization 'flip2<void (*)(int, int &&), int, int &>' requested here
      flip2(f3, 1, val2); // as expected, because T2 is deduced as int&(int& && -> int&)
    • 终极解决方案 std::forward

      std::forward 可以保持参数的原始类型

      • forward 必须通过显式实参调用
      • forward 返回该显式参数类型的右值引用,即 forward<T> 返回的都是 T&&
        • 当 t2 是右值的时候,T2 推导为 int,此时 forward 的返回值为 int&&
        • 当 t2 是左值的时候,T2 推导为 int&,此时 forward 的返回值为 int& &&,折叠为 int&
      • 当用于一个指向模板参数类型的右值引用 T&& 时,forward 会保持实参类型的所有细节
      1
      2
      3
      4
      5
      6
      7
      template<typename F, typename T1, typename T2>
      void flip3(F f, T1&& t1, T2&& t2){
      f(std::forward<T1>(t1), std::forward<T2>(t2));
      }

      void f3(int v1, int&& v2){std::cout << v1 << " " << ++v2 << std::endl;}
      flip3(f3, 1, 1); // valid, compile pass
      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
      29
      /// std::forward STL source code: gcc/libstdc++-v3/include/bits/move.h

      /**
      * @brief Forward an lvalue.
      * @return The parameter cast to the specified type.
      *
      * This function is used to implement "perfect forwarding".
      */
      template<typename _Tp>
      _GLIBCXX_NODISCARD
      constexpr _Tp&&
      forward(typename std::remove_reference<_Tp>::type& __t) noexcept
      { return static_cast<_Tp&&>(__t); }

      /**
      * @brief Forward an rvalue.
      * @return The parameter cast to the specified type.
      *
      * This function is used to implement "perfect forwarding".
      */
      template<typename _Tp>
      _GLIBCXX_NODISCARD
      constexpr _Tp&&
      forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
      {
      static_assert(!std::is_lvalue_reference<_Tp>::value,
      "std::forward must not be used to convert an rvalue to an lvalue");
      return static_cast<_Tp&&>(__t);
      }

      因为 std::forward 的返回值是_Tp&&

      • 当_Tp 是 int& 的时候,std::forward 的返回值为 int&
      • 当_Tp 是 int&& 的时候,std::forward 的返回值为 int&&

Overloading and Template

  • 模板函数的重载和匹配
    • 对于一个函数调用,其函数匹配包含所有模版的实参类型自动推断成功的函数模版

    • 和往常一样,可匹配的函数按照类型转换的优先级先后排序

      • 非模板函数按照正常的类型转换去匹配,参见 chapter6
      • 模板函数的自动推导中使用的类型转换类型有限,参见 chapter16
    • 和往常一样,如果只有一个函数可以提供比其他函数更好的匹配,则选择这个函数,否则:

      • 优先选择非模版函数:如果同样好的函数里面只有一个非模板函数,则选择这个非模板函数 (round 4)
      • 优先选择更为特例化的模版函数:如果同样好的函数里面没有非模板函数,而有多个函数模板,且其中的一个模板比其他模板更特例化,则选择这个模板 (round3 and round5),比如不可变参模板比可变参数模板更特例化;
      • 否则此调用有歧义 (ambiguous)
    • 通常如果忘记了声明函数而用了它,编译器会报错;但是对于重载函数模版而言则不是这样:

      如果编译器可以从模板实例化出与调用匹配的版本,则缺少我们希望使用的函数的申明也不会报错;事实上,编译器用了一个并不是我们希望使用的函数,这和预期不一致,会导致不易发现的错误

    1
    2
    3
    4
    5
    6
    7
    8
    // template function 1, can match lvalue or rvalue
    template <typename T>
    std::string debug_rep(const T&);

    // template function 2, can only match pointer
    template <typename T>
    std::string debug_rep(T*);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // round 1
    std::string s("hi");
    debug_rep(s); // only template 1 is matched, because s is not a pointer

    // round 2
    debug_rep(&s); // candidates are: template 1. debug_rep(const std::string* &):
    // need to convert std::string* to const std::string&
    // template 2. debug_rep(std::string *): precise match
    // so template 2 is precisely matched
    // round 3
    std::string& sp = &s;
    debug_rep(sp); // candidated are: template 1, debug_rep(const std::string* &);
    // template 2, debug_rep(std::string *);
    // because template 2 is more specicalized than template 1, than template 2 is matched
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // normal non-template function
    std::string debug_rep(const std::string&);

    // round 4, consider non-template fucntion
    debug_rep(s); // candidate are 1. template 1, debug_rep(const std::string &);
    // 2. non-template function, debug_rep(comst std::string&);
    // though the two candicates all matches precisely, non-template function
    // is prefered,so non-template function is matched

    // round 5
    debug_rep("hello world");
    // candicated are: 1, debug_rep(const char[10] &), T is char[10],
    // string literal is char array
    // 2, debug_rep(const char*), T is const char
    // 3, debug_rep(const std::string&), convert char* to std::string
    // because candidate 3 has the need to do type convert(char* to string), so it is
    // not better matched than candidates 1 and 2, also candicate 2 is more specialized
    // than candidate 1, so candidate 2 is matched

Variadic Template

  • 参数包 (pack)

    可变数目的参数被称为参数包,可以使用省略号... 代表参数包;存在两种参数包:

    • 模板参数包:在模板参数列表里面,class... or template... 表示 0 个或多个类型
    • 函数参数包:一个类型名后面更一个... 表示 0 个或者多个此给定类型的参数
    1
    2
    template <typename T, typename ... Args>;     // Args 是一个模板参数包,表示模板可以接受0个或者多个类型
    void foo(const T&, const Args&... residual); // residual是一个函数参数包,表示0个或者多个参数
  • 可以使用 sizeof... 运算符得到可变类型和可变参数的数量

    1
    2
    3
    4
    5
    6
    7
    template <typename ... Args>
    void foo(Args... args){
    std::cout << sizeof...(Args) << std::endl;
    std::cout << sizeof...(args) << std::endl;
    }

    foo("1", 1, 1.0); // print 3, 3
  • 可变参数的函数通常是递归的

    为了终止递归,需要定义一个非可变参数的函数模板;因为非可变参数函数比起可变参数函数是更加特例化的,所以当两种函数都可以精确匹配的时候,优先考虑非可变参数函数;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 用于终止递归, 并打印最后一个元素
    // 必须在可变参数版本之前声明, 若在之后声明, 则无法通过编译,
    template <typename T>
    std::ostream& print(std::ostream& os, const T&t){return os << t;}

    // 可变参数函数通常是递归的,每一步处理最前面的一个实参, 然后用剩余参数调用自身.
    // 除了最后一个元素之外的其他元素都会调用这个版本的print
    template <typename T, typename... Args>
    std::ostream& print(std::ostream& os, const T& t, const Args&... residual){ // Args&...包扩展,见下节解释
    os << t << " , ";
    return print(os, residual...); // residual...包扩展
    }

    针对以上代码:

    非可变参版本的声明必须在可变参函数之前,否则可变参版本的 print 将会一直调用自身直到耗尽参数包,仅用一个 os 参数去调用 print,会无法通过编译

    note: candidate expects at least 2 arguments, 1 provided

    最后一次调用,参数就只有 os 和一个待打印值,所以以上两个模板都可以匹配;但是因为不可变参数模板更特例化,所以匹配非可变参数模板

  • 参数包扩展 (pack expansion)

    对于一个参数包 (上面的 redidual 参数),我们除了获取其大小 (sizeof...) 外,唯一能做的就是扩展它 (expand);当扩展一个参数包的时候,我们还需要提供用于每个扩展元素的模式, 这里的模式通常为一些类型限定修饰符;

    扩展包就是对包中的每一个元素都应用一个指定的模式,并得到展开后的逗号分隔的列表;通过在模式右边放一个省略号 (...) 触发扩展操作

    扩展模版参数包:

    1
    std::ostream& print(std::ostream& os, const T& t, const Args&... residual){ //Args&...包扩展,见下节解释

    const Args&... residual 扩展模板参数包,将 const type& 应用到包中的每一个元素,为 print 生成参数列表。

    1
    print(cout, 2, 3.14, "asd"); // 包中有两个参数

    此调用被实例化为

    1
    ostream& print(ostream&, const int&, const double&, const char[4]&);

    扩展函数参数包:

    1
    return print(os, residual...);        // residual...包扩展

    在 print 函数的 return 语句中的递归调用也触发了扩展 (有... 出现),只是它直接将 residual 扩展为逗号分隔的参数列表 , 而没有改变各个元素本身的类型。

    考虑更清晰的一个例子,有这样一个函数:

    1
    2
    3
    4
    5
    6
    template <typename T>
    string debug_rep(const T& t){
    ostringstream ss;
    ss << t;
    return ss.str();
    }

    它通过流操作符从传进来的参数获取一个字符串.

    1
    2
    3
    4
    template <typename... Args>
    ostream &errorMsg(ostream&os, const Args&... rest) {
    return print(os, debug_rep(rest)...);
    }

    errorMsg 将传进来的参数包 rest 用 debug_rep 扩展

    1
    errorMsg(cerr, fcnName, code.num(), otherData, "balabala", item);

    扩展结果:

    1
    2
    3
    print(cerr, debug_rep(fcnName), debug_rep(code.num(),
    debug_rep(otherData), debug_rep("balabala"),
    debug_rep(item)));

    相对的:

    1
    print(os, debug_rep(rest...));

    展开为将包传递给了 debug_rep:print(os, debug_rep(a1,a2,a3...an)),所以这个调用会失败,debug_rep 没有匹配的参数列表。

  • 转发参数包:新标准下可以组合使用std::forward 和参数包扩展,比如 std::vector::emplace_back 就是可变参数

    1
    2
    3
    4
    5
    template <typename Args...>
    inline void StrVec::emplace_back(Args&&... args){
    check_n_alloc();
    alloc.construct(first_free++, std::forward<Args>(args)...);
    }

    std::forward<Args>(args)...既扩展了模板参数包Args又拓展了函数参数包args,生成了 std::forward<T_i>(args_i) 形式的元素;

    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
    29
    30
    31
    32
    // std::vector emplace STL实现:gcc/libstdc++-v3/include/bits/vector.tcc
    template<typename _Tp, typename _Alloc>
    template<typename... _Args>
    _GLIBCXX20_CONSTEXPR
    auto
    vector<_Tp, _Alloc>::
    _M_emplace_aux(const_iterator __position, _Args&&... __args)
    -> iterator
    {
    const auto __n = __position - cbegin();
    if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage)
    if (__position == cend())
    {
    _GLIBCXX_ASAN_ANNOTATE_GROW(1);
    _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
    std::forward<_Args>(__args)...);
    ++this->_M_impl._M_finish;
    _GLIBCXX_ASAN_ANNOTATE_GREW(1);
    }
    else
    {
    // We need to construct a temporary because something in __args...
    // could alias one of the elements of the container and so we
    // need to use it before _M_insert_aux moves elements around.
    _Temporary_value __tmp(this, std::forward<_Args>(__args)...);
    _M_insert_aux(begin() + __n, std::move(__tmp._M_val()));
    }
    else
    _M_realloc_insert(begin() + __n, std::forward<_Args>(__args)...);

    return iterator(this->_M_impl._M_start + __n);
    }

Template Specialization

  • 全 (局) 特化 (full specialization)

    将模板所有的类型都进行特化,应使用关键字 template 后跟一对空尖括号 <>;空尖括号指明我们将为原模版的所有模版参数都提供实参;

    在使用任何模版实例化的代码之前,特例化版本的代码的声明也必须在作用域中,否则在实例化的时候编译器将无法使用特例化版本;模版及其特例化版本应该声明在同一个头文件中;所有同名模版的声明应该放在前面,然后是这些模版的特例化版本;

    当定义函数模版的特例化版本的时候,本质是接管了编译器的工作实例化了一个函数模版,而非重载它;因此函数模版的特例化是不影响函数的匹配的;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    template <typename T> int compare(const T&, const T&);

    template<> // 函数模版全特化
    int compare(const char* const &, const char* const &){
    }

    template <class T1, class T2>
    class Test{
    };

    template <> // 类模版全特化
    class Test<int , char>{
    };

    和继承不同,类模板的 “原型” 和它的特化类在实现上是没有关系的,并不是在类模板中写了 ID 这个 Member,那所有的特化就必须要加入 ID 这个 Member,或者特化就自动有了这个成员。完全没这回事。

    类模板和类模板的特化的作用,仅仅是指导编译器选择哪个编译,但是特化之间、特化和它原型的类模板之间,是分别独立实现的。实际上,全特化和类模板只是名称有关联。

    我们把类模板改成以下形式,或许能看的更清楚一点:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
      
    template <typename T>
    class TypeToID {
    public:
    static int const NotID = -2;
    };

    template <>
    class TypeToID<float> { // 全特化
    public:
    static int const ID = 1;
    };

    TypeToID<float>::ID; // Print "1"
    TypeToID<float>::NotID; // Error! TypeToID<float>使用的特化的类,这个类的实现没有NotID这个成员。
    TypeToID<double>::ID; // Error! TypeToID<double>是由模板类实例化出来的,它只有NotID,没有ID这个成员。

    特例化的模板类的实参列表必须和相应的基础模板参数列表一一对应。例如,我们不能用一个非类型值来替换一个模板类型参数。然而,如果模板参数具有缺省模板实参,那么用来替换的模板实参就是可选的(即不是必须的)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    template<typename T>
    class Types {
    public:
    typedef int I;
    };

    template<typename T, typename U = typename Types<T>::I>
    class S;          

    template<> class S<void> {     // valid   
    public:
    void f();
    };
    template<> class S<char, char>; // valid
    template<> class S<char, 0>; // 错误:不能用0来替换typename U

    对于特化声明而言,因为它并不是模板声明,所以应该使用(位于类外部)的普通成员定义语法,来定义全局类模板特化的成员(也就是说,不能指定 template<> 前缀)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template<typename T>
    class S;

    template<> class S<char**> {
    public:
    void print() const;
    };

    //下面的定义不能使用template<>前缀
    void S<char**>::print() const {
    std::cout << "pointer to pointer to char\n";
    }

    我们可以只特例化特定成员而不是这个模板;类的成员模板、类模板的成员函数和普通的静态成员变量也可以被全局特化;

    实现特化的语法会要求给每个外围类模板加上 template<> 前缀。如果要对一个成员模板进行特化,也必须加上另一个 template<> 前缀,来说明该声明表示的是一个特化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template <typename T>
    struct Foo{
    Foo(const T&t = T()): mem(t){/* do something */}
    void Bar(){/* do something */};
    T mem;
    };

    template<>
    void Foo<int>::Bar(){/* do something */}

    Foo<std::string> fs; // 实例化Foo<std::string>::Foo()
    fs.Bar(); // 实例化Foo<std::string>::Bar()
    Foo<int> fi; // 实例化Foo<int>::Foo()
    fi.Bar(); // 实例化我们特例化的Foo<int>::Bar()
  • 偏特化 (partial specialization):

    偏特化分为两种,一种是部分类型特化,一种是对模板类型的进一步限制

    1
    2
    3
    4
    5
    6
    7
    template <class T1, class T2>
    class Test2{
    }
    //部分特化
    template <class T1> //此处只需写未进行特化的模板类型,特化过的就不用写
    class Test2<T1 , char>{
    }
    1
    2
    3
    4
    5
    6
    7
    template <class T1, class T2>
    class Test2{
    }
    //对模板类型的范围做出一定的限制,限制实参为T*指针
    template <class T1 , class T2 > //此处只需写未进行特化的模板类型
    class Test2<T1* , T2*>{
    }

    考虑我们要写一个向量逐分量乘法。只不过这个向量,它非常的大。所以为了保证速度,我们需要使用 SIMD 指令进行加速。假设我们有以下指令可以使用:

    1
    2
    3
    4
    Int8,16: N/A
    Int32 : VInt32Mul(int32x4, int32x4)
    Int64 : VInt64Mul(int64x4, int64x4)
    Float : VInt64Mul(floatx2, floatx2)

    所以对于 Int8 和 Int16,我们需要提升到 Int32,而 Int32 和 Int64,各自使用自己的指令。所以我们需要实现下的逻辑:

    1
    2
    3
    4
    5
    6
    7
    8
    for(v4a, v4b : vectorsA, vectorsB) {
    if type is Int8, Int16
    VInt32Mul( ConvertToInt32(v4a), ConvertToInt32(v4b) )
    elif type is Int32
    VInt32Mul( v4a, v4b )
    elif type is Float
    ...
    }

    如何根据 type 分别提供我们需要的实现?这里有两个难点。

    • 首先, if(type == xxx) {} 是不存在于 C++ 中的。
    • 第二,即便存在根据 type 的分配方法,我们也不希望它在运行时 branch,这样会变得很慢。我们希望它能按照类型直接就把代码编译好,就跟直接写的一样。

    部分特化 / 偏特化 和 特化 相当于是模板实例化过程中的 if-then-else。这使得我们根据不同类型,选择不同实现的需求得以实现;

    SFINAE(Substitution failure is not an error )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template <typename T>
    class RemovePointer {
    };

    template <typename T>
    class RemovePointer<T*> {
    public:
    typedef T Result;
    };

    // 用RemovePointer后,那个Result就是把float*的指针处理掉以后的结果:float啦。
    RemovePointer<float*>::Result x = 5.0f;

    如何理解偏特化的写法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 我们这个模板的基本形式是什么?
    template <typename T> class AddFloatOrMulInt;

    // 但是这个类,是给T是Int的时候用的,于是我们写作
    class AddFloatOrMulInt<int>
    // 当然,这里编译是通不过的。

    // 但是它又不是个普通类,而是类模板的一个特化(特例)。
    // 所以前面要加模板关键字template,
    // 以及模板参数列表
    template </* 这里要填什么? */> class AddFloatOrMulInt<int>;

    // 最后,模板参数列表里面填什么?因为原型的T已经被int取代了。所以这里就不能也不需要放任何额外的参数了。
    // 所以这里放空。
    template <> class AddFloatOrMulInt<int> {
    // ... 针对Int的实现 ...
    }

    Reference: https://sg-first.gitbooks.io/cpp-template-tutorial/content/

    除了简单的指针、const 和 volatile 修饰符,其他的类模板也可以作为偏特化时的 “模式” 出现,例如示例 8,它要求传入同一个类型的 unique_ptr 和 shared_ptr

    1
    2
    3
    4
    5
    6
    template <typename T, typename U> 
    struct X;

    // 偏特化
    template <typename T>
    struct X<unique_ptr<T>, shared_ptr<T>>;
  • 函数模板特例化

    • 为了指明我们正在特例化一个模板,应使用 template 关键字后跟上 <> 空的尖括号;空的尖括号表示会为所有参数提供实参

    • 当我们特例化一个函数模板的时候,必须为原模板中的每个参数提供实参,不可部分指定;且参数类型必须与前面声明的模板中对应的类型匹配

    • 当定义了函数模板的特例化版本时,本质上是接管了编译器该做的工作,即我们为模板的某个特殊的实例提供了定义;所有对函数模板的特例化,本质上是实例化一个模板,而不是对原模板的重载;所以对函数模板的特例化并不影响模板的匹配

      函数模版的全特化版本不参与函数重载解析,并且优先级低于函数基础模版参与匹配的原因是:*C++* 标准委员会认为如果因为程序员随意写了一个函数模版的全特化版本,而使得原先的重载函数模板匹配结果发生改变(也就是改变了约定的重载解析规则)是不能接受的。

    • 使用模板特化时,必须要先有基础的模板函数

      未特化的模版通常也叫做底层基础模版。函数模版的全特化到底是哪个函数基础模版的特化,需要参考可见原则,也就是说当特化版本声明时,它只可能特化的是当前编译单元已经定义的函数基础模版。

    • 类模版可以偏特化和全特化,而函数模版只能进行全特化,但是由于函数模版可以重载,我们通过重载可以获得和偏特化几乎相同的效果。

    • 我们可以直接写非模板函数来重载函数模板,这样的非模板函数在函数匹配上比模板的优先级更高;所以很多时候,要避免函数模板的特例化,直接重载函数就好了 reference: Why Not Specialize Function Templates?

    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
    // 基础的模板函数
    template <typename T>
    int compare(const T&, const T&){
    std::cout << "generic compare" << std::endl;
    }

    // 特化模板函数
    template<>
    int compare(const int&, const int&){
    std::cout << "int compare" << std::endl;
    }
    template<int M, int N>
    int compare(const char(&)[M], const char (&)[N]){
    std::cout << "char array compare" << std::endl;
    }
    template <>
    int compare(const char* const &, const char* const &){
    std::cout << "const char* const compare" << std::endl;
    }

    const char* p1 = "123";
    const char* p2 = "456";
    compare(p1, p2); // const char* const compare
    compare(1, 2); // int compare
    compare(true, true); // generic compare
    compare("hi", "mom"); // char array compare

    如何理解:参数类型必须与前面声明的模板中对应的类型匹配:

    如果把对 const char* 特例化的模板写成:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    template <>
    int compare(const char* const, const char* const){
    std::cout << "const char* const compare" << std::endl;
    }
    // or
    template <>
    int compare(const char* &, const char* &){
    std::cout << "const char* const compare" << std::endl;
    }

    编译器会报错:

    1
    2
    3
    4
    5
    m.cpp:44:5: error: no function template matches function template specialization 'compare'
    int compare(const char* const, const char* const){
    ^
    m.cpp:34:5: note: candidate template ignored: could not match 'const type-parameter-0-0 &' against 'const char *'
    int compare(const T&, const T
    1
    2
    3
    4
    5
    m.cpp:44:5: error: no function template matches function template specialization 'compare'
    int compare(const char* &, const char* &){
    ^
    m.cpp:34:5: note: candidate template ignored: cannot deduce a type for 'T' that would make 'const T' equal 'const char *'
    int compare(const T&, const T&){

    这是因为原来的模板里面参数为 const T&,是底层 const,而 pointer 前面的 const 是顶层 const,后面的才是底层 const

    所以对 char* 的特例化的模板参数应该写为 const char* const &;

    如果模板参数写成 char* const & 可以么?

    不可以,如果模板参数是 char* const & 的,则无法匹配实参类型为 const char* 的调用,即:

    1
    2
    const char* p1 = "123", * p2 = "456";    
    compare(p1, p2); // will print generic compare, not const char* const compare

    如果手工写非模版函数重载函数模板,函数的匹配又会变化:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 基础的模板函数
    template <typename T>
    int compare(const T&, const T&){
    std::cout << "generic compare" << std::endl;
    }

    // 特化模板函数
    template <>
    int compare(const char* const &, const char* const &){
    std::cout << "const char* const compare" << std::endl;
    }

    // 非模版函数,重载函数模板
    int compare(const char* const &, const char* const &){
    std::cout << "non-template const char* const compare" << std::endl;
    }

    const char* p1 = "123";
    const char* p2 = "456";
    compare(p1, p2); // non-template const char* const compare
    compare("hi", "mom"); // non-template const char* const compare

    这是因为非模板函数的匹配优先级比模板函数更高 (chapter16),而且模板的特例化并不影响模板的匹配