C++ Primer(第五版)第十六章 - 模板与泛型编程。

  1. 泛型与OOP

    泛型与OOP都能处理在编写程序时不清楚具体类型的情况。

    OOP是通过继承+动态绑定;而泛型则是通过编写类型独立的代码,并且在编译时实例化模板完成。

  2. 泛型编程的基础是模板。模板可以解释为一个创建类或函数的蓝图或公式。

    单纯的模板名不是类!

  3. 模板参数列表

    模板参数列表可以类比函数参数列表。函数参数(形参)在调用时初始化,而模板参数在实例化模板时显示或隐式指定。

    1. 通过template<XXX>来指定模板参数列表。模板参数列表在定义模板时必须不为空。

    2. 模板参数有两类,一类是模板类型参数,通过 typename或者class指定(二者完全等价,class是在typename没有出来前所使用的名称),通过此参数可以定义类型;另一类称为非类型模板参数,即通过常规类型 + 名称指定,定义的是值。

      综上,模板参数列表可以定义类型、也可以定义值。如下:

       template<typename T, size_t N>
       constexpr size_t size(const T (&a)[N])
       {
           return N;
       }
      
    3. 传递给模板参数列表中的值,要么是整型字面值,要么是指向静态区变量的指针或引用!

      1. 必须是整型(bool,short, int, long 等)

        看一个double的例子:

         template <double x>
         void getX()
         {
             cout << x << endl;
         }
         编译报错简直太长太长(模板编程的缺点)...但是第一个错误就是:
         'double' is not a valid type for a template non-type parameter
        

        其实实数本来作为模板参数意义也不大,而且实数因为在不同机器表示可能不同,因此在编译时是没法确定大小的??(不对,sizeof就可以啊!!到底是什么原因?)

      2. 静态区变量的指针或引用

        首先必须是引用或指针;

        其次必须是静态区的变量——即全局变量,静态变量。

      一句话,必须编译时可以确定其值。

  4. 大多数编译错误发生在实例化期间

    因为不实例化时编译器并不会生成代码。

  5. 模板声明与定义需要在同一个文件内(头文件)

  6. 尽量编写类型无关的代码

    即适用性更高,对传入类型依赖更小。

    比如几个可能的需求:

    1. 使用 const & , 这样可以处理数组

    2. 只使用 < (准确的说,使用 less等即可)

  7. 模板函数可通过编译器自动推导参数类型,而模板类则只能通过用户在模板名<XXX>中的XXX来指定模板参数. XXX称为显式模板实参。

  8. 类模板的实例化

     template <typename T>
     class ClassT
     { ... };
    

    使用ClassT<int> t;之后实例化的结果为:

     template <>
     class ClassT<int>
     { ... };
    
  9. 模板类的成员函数只有当程序用到它时才会实例化。

    所以即使一个错误的、未使用的模板类成员函数存在,也不会报错。

  10. 在模板类作用域内部可以省略模板实参

    这个与类作用域内不必使用类作用域限定符同理。

    当我们在类作用域外部定义类成员是,应当如此定义:

     template <typename T>
     ClassT<T> ClassT<T>::foo(ClassT &param)
     {
         ClassT newClass;
         ...
     }
    

    正如前面章节提过的,对签名 ret-type func-name(param-list){ func-body }, ret-typefunc-name都是在作用域外部,故需要使用作用域限定符!因此使用模板名+模板参数列表, 从函数实参列表开始,都算作类作用域内部,可以不再使用模板实参而直接用模板名替代。

    当然,无论何时,使用完整的名字总是不会有问题的!

    不过,我们应该在表意清晰的情况下,尽可能简化我们的代码!

  11. 模板类和友元

    模板类与模板类间以一相同实例的1 VS 1

    template <typename T>
    class ClassT<T>;
    
    template <typename T>
    class FriendT<T>
    {
        friend class ClassT<T>;
    };
    

    即用friend class 声明时使用同一模板参数T!因为这样定义的话,其实对FriendT<T>, 其友元类是确定的:ClassT<T> .

    模板类与模板类间任意 VS 任意

    template <typename T>
    class FriendT<T>
    {
        template <typename X>
        friend class ClassT;
    };
    

    这里一定要注意!任意对任意时,friend class前需要template <typename X>,此时模板变量应该与外层模板的模板变量不一致(外层是T); 此外,friend class ClassT; 友元类后面一定不能加模板实参!加了就会报错!最后,ClassT也不必再前面提前声明。

    我觉得这个地方太容易错了。

    最后,C++11增加了一个特性——可以令模板自己的类型为友元。即

    template <typename T>
    class ClassT
    {
        friend class T;
    };
    

    这说明,允许内置类型为友元类(语法上的允许,实际上没有任何用处…)。

  12. 模板类型别名

    可以用typedef来重命名实例化的模板.

    typedef ClassT<int> ClassInt;
    

    C++11中,增加了使用using 来重命名模板.

    template <typename T>
    using AnotherName = ClassT<T>;
    
    template <typename T>
    using twins = pair<T, T>;
    template <typename T>
    using partNo = pair<T, unsigned>;
    

    可以看到这非常地灵活(有点相当于std::bind中功能)!同时,这也非常的直观。注意,该功能是typedef所不支持的。

    从这个角度说, 真的可以放弃 typedef.

  13. 模板参数名的作用域

    模板参数名的可用范围是:参数名声明以后到模板声明或定义结束。

    // 1
    template <typename T, typename F=less<T>> int compare(const T &v1, const T &v2, 
                                                          F f=F());
    // 2
    template <typename T> class X
    {
        T t;
    };
    

    即是说,从声明这个名字开始,到同级一个表达式结束(只是想说,在模板类外部定义成员函数,需要在每个成员函数前写上template )。

    另外,关于模板名字的覆盖:

    1. 模板名字可以覆盖前面的非模板名字

    2. 模板内部作用域下,不能再覆盖模板名字。

    两个实例:

    // 1. 模板名字可以覆盖前面的非模板名字 (这里外部名字是变量名,也可是typedef的类型名)
    int T = 10;
    template <typename T, typename F=less<T>>
    
    // 2. 模板内部作用域下,不能再覆盖模板名字。
    template <typename T, typename F=less<T>>
    int compare(const T &lhs, const T &rhs, F f = less<T>())
    {
        int T = 10;
        //...
    } -> 报错: error:  shadows template parm 'class T'
    
  14. 模板名字不重要

    如书上的例子:

    template <typename T> T calc(const T&, const T&);
    template <typename U> U calc(const U&, const U&);
    template <typename Type>
    Type calc(const Type &lhs, const Type &rhs)
    {
        // ...
    }
    

    上述3个均是同一个函数模板签名。前两个是声明,后一个包含定义。

  15. 使用类内成员(如果是类型一定要加typename

    如果模板类型参数T,其有类内成员,如下的写法:

    T::val

    表示val是类型T的类静态(公有)成员;如果要表示类内的一个类型,需要使用前缀关键字typename:

    typename T::type XXX.

  16. 模板参数列表也可以使用默认参数!

    使用的方式和如前面的例子:

    template <typename T, typename F=less<T>>
    

    遵循的规则与函数形参中默认参数相同:默认参数之后要么全是默认参数,要么没有额外参数。

    最后,对于函数模板,如果全部有默认参数,那么调用时就可以省略模板实参(因为模板函数有自动类型推导过程!);但是对模板类,即使全有默认值了,也需要一个空的尖括号表示实例化!

  17. 类的成员函数是模板函数的情况。

    本身是模板的成员函数被称为成员模板。

    对于一个非模板类,其成员可以是模板函数。这没有什么新奇的。在声明和定义时按照普通模板函数写即可。

    对于模板类的模板函数,想想如果在类外定义,该怎么办呢?很简单,此时模板作用域发生嵌套——先写类模板声明,再写成员函数的模板声明。

    template <typename ClassT>
    template <typename FuncT>
    void TClass<ClassT>::member_template(FuncT &);
    

    什么时候会用到模板类下还有模板函数呢? 之前写代码时,需要用Boost实例化一个模板类,这必然得用到,因为接口是一个模板函数;容器类,其构造函数包含一个迭代器的版本,这个迭代器可以是任意的(与本身类型不同),所以也必须得是模板函数!

  18. 控制实例化

    这个之前真没遇到过。

    模板只有在使用时才实例化,这种惰性方法,在多编译单元时可能导致二进制代码冗余、编译时间变长: 如果两个obj文件,都使用了同一个类型的模板,那么其各自编译时都得在自己的编译代码中生成相应的实例化代码。这显然是很冗余、不必要的。

    所以,或许我们需要显式实例化!

    所谓显式实例化,通常这样:

    1. 在一个单独编译单元中显示实例化,称为实例化定义.

      举个例子:

       // instance.cpp
       template class vector<int>;
       template int make_pair(int &&i , int &&j);
      
    2. 在其他需要实例化的地方使用外部定义的版本。

      具体来说,在需要使用之前(最好是头部),使用:

       extern template class vector<int>;
       extern template int make_pair(int &&, int &&);
      

    然后又有一个很不一般的地方需要注意:在前面提到过,普通实例化类时,也是惰性实例化的,即需要用到哪个函数,才实例化哪个成员函数。然而在实例化定义时,编译器将一次性将类的所有成员函数全部实例化——因为编译器不知道你要用哪些,所以只能全部实例化了。

    因而,编译器在需要实例化时才实例化被称为非显示实例化(implicit instantiation), 而用实例化定义的方式实例化,称为显示实例化(explicit instantiation).

    看看CPPReference网站上关于非显示实例化的例子:

    template<class T> struct Z {
    void f() {}
    void g(); // never defined
    }; // template definition
    template struct Z<double>; // explicit instantiation of Z<double>
    Z<int> a; // implicit instantiation of Z<int>
    Z<char>* p; // nothing is instantiated here
    p->f(); // implicit instantiation of Z<char> and Z<char>::f() occurs here.
    // Z<char>::g() is never needed and never instantiated: it does not have to be defined   
    

    声明一个指针并不会有任何实例化操作,但是对指针做操作就会带来非显示实例化。

  19. shared_ptruniq_ptr看模板设计的灵活与效率的抉择

    这个实在是一个很神奇的例子。目前我还是没有想明白。

    shared_ptr中,我们设置删除器,可以在构造一个指针时(构造函数指定),也可以在reset一个shared_ptr时。这表明,对于shared_ptr, 删除器是在运行时绑定的,使用户重载删除器更加方便。

    uniq_ptr的删除器是作为模板参数类型的一部分,即是说删除器类型是uniq_ptr模板实例化的类的一部分,而非某个对象的一部分。因而,在删除一个对象时,我们可以直接调用删除器,而对于shared_ptr,因为不知道删除器是否被设置,所以必须做一个if判断。这说明使用uniq_ptr的效率优于shared_ptr. 不过把删除器作为类型的一部分,导致编码不是那么的灵活。

    书里面提到了因为shared_ptr因为删除器不是作为类型存在,所以不会是类成员,而uniq_ptr中会是一个类成员。但是,如果删除器不是一个类成员,那么又是用什么来保存的呢?感觉就没有其他方法了啊!习题就是实现自己的shared_ptruniq_ptr, 感觉以后必须得做一下。

    立足当前,我先把Primer刷完。

    update:

    看了下shared_ptr的构造函数接口,发现Deleter在某些构造函数中是作为模板参数存在的——shared_ptr构造函数非常复杂,即有非模板函数,又有模板函数,其中针对多态、数组等类型,还会有多余的模板类型Y(尽管在实例化shared_ptr模板类时,已经声明了模板参数)

  20. 在模板实参推断中只包含非常少的类型转换(可以认为几乎没有)

    经常在代码中写错的一个例子:

    static const int MAXV = 20;
    int maxVal = max(container.size() , MAXV);
    

    以上会报错,因为.size()返回的是Container::size_type,一般就是unsigned long long(size_t)了,与int不一致。我们知道max的一个模板声明为:

    template <typename T>
    inline max(const T&, const T&);
    

    两个参数的类型应该是一致的,且在模板推导过程中只会做很少的类型转换,这很少的类型转换就不包含算术类型转换,所以根据模板实参推断,得到的调用为: max(const size_t &, const int &),显然两个类型是不一样的,不能绑定到一个T上,因此失败。

    以上是一个例子,下面是书上的具体说法。

    如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。只有很有限的几种类型转换会自动地应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。

    能够支持的转换有:

    1. const转换 : 可以将非const的指针或引用传递一个const的引用或指针,或const值传递给非const值。

    2. 数组或函数指针转换 : 如果函形参不是一个引用类型,那么可以对数组或者函数做指针转换,即数组退化为指针,函数变为函数指针。

    个人觉得: 之所以支持上面两种转换,其实是因为这种转换也许在编译器看来就是等价的(不需要转换就行)—— 顶层const在形参或实参中均会被忽略,而数组退化为指针是兼容C,函数变为指针完全就是混用(兼容C?)。(以上这段没有根据)

    算术转换、派生类向基类转换(这说明模板函数不会形成多态调用?)、用户定义的转换都不会被执行。

    最后,模板函数中非模板参数类型,即普通类型,仍然支持通用的转换。

  21. 为模板函数使用显示模板实参

    有时我们无法根据函数的形参列表就推断出模板函数所需要的全部类型,于是就需要使用显示模板参数。

    make_shared模板函数的接口:

    template<class T, class... Args>
    make_shared<T>(Args&&... args)
    

    这是因为args是T的构造函数参数,即T是不会被包含在函数模板的参数列表中,故只能通过显示函数模板实参给出。

    此外,因为const T& max(const T&, const T&),所以max要求两个参数类型必须一致,因为在求xxx.size()和某int值的max时,我们都需要使用显式转换使其一致。我们可以使用以下方式来避免显式转换:

    template <typename T1, typename T2, typename T3>
    const T1& max(const T2&, const T3&)
    

    这样只要给max显式传一个返回类型T1,其余两个模板都可以自动推导出来。需要注意的是,给模板参数列表传值同样遵循从左往右的顺序,只能省略右边连续的有默认值的模板参数。此外,比较下这两种max,应该还是标准库的更好吧——类型转换都是必须的(内部比较),但是额外的返回类型其实没有必要的。

  22. 小技巧——接受迭代器的函数将迭代器指向的类型作为返回类型,可以使用尾置返回类型+decltype

    写得有点长,用代码展示的话,就是这样:

    template <typename It>
    auto -> fcn(It beg, It end) -> decltype(*beg)
    { 
        //... 
    }
    

    这个需求在平时还是比较常见的,曾近在LeetCode中想要编写接受迭代器并且返回类型的函数时,发现没法知道迭代器指向的类型。

    上述方法非常不错。关键是说明了一点,就是编译器真是很强大的 —— 只要前面见过这个符号,那么我们在后面就可以使用它了!所以我们才需要使用尾置返回类型,因为在处理返回类型前,beg已经有了确切的值!这时才有可能使用decltype.

    当然,通用的,获取迭代器指向类型的方法是:

    #include <Iterator>
    
    template <typename It>
    typename std::iterator_traits<It>::value_type fcn(It beg, It end)
    {
        //...
    }
    

    即使用<Iterator>头文件中实现的iterator_traits<Iterator>::vlaue_type即可。该类对于普通指针做了特例化,可以支持普通指针,因此实现了指针与迭代器操作上的统一。

    最后需要注明的是,decltype(*beg)返回的类型是一个左值引用类型!将这种类型作为返回值,一般是非常危险的!,所以还是推荐使用iterator_traints<It>::value_type,或者使用下面介绍的模板类型转换,将引用属性去掉(remove_reference<T>::type / remove_reference_t<T>)。

  23. 模板类型的类型转换、检查等

    定义在<type_traits>头文件中的模板类型检查、转换实在太厉害了。现在还不知道其内部是怎么实现的啊!!

    首先看下cppreference上列出来的支持操作吧(这里直接COPY下来好像不太好,就不COPY了,只列出类别及一个例子)。

    // primary type categories:
    template <class T> struct is_function;
    
    // composite type categories:
    template <class T> struct is_reference;
    
    // type properties:
    template <class T> struct is_const;
    
    // type property queries:
    template <class T> struct alignment_of;
    
    // type relations:
    template <class Base, class Derived> struct is_base_of;
    
    // const-volatile modifications:
    template <class T> struct remove_const;
    
    // reference modifications:
    template <class T> struct remove_reference;
    
    // sign modifications:
    template <class T> struct make_signed;
    
    // array modifications:
    template <class T> struct remove_extent;
    
    // pointer modifications:
    template <class T> struct remove_pointer;
    
    // other transformations:
    template <bool, class T = void> struct enable_if;
    

    实在太强大了,有了这些工具,还有什么不能实现吗?这是不是与Java、Python中的类型检测的能力想同呢?之前一直不知道C++支持这样看起来如此高级的特性!真是开眼界了。

    有空补上实现细节。

习题

  1. typename 与 class 有什么不同,什么时候必须使用typename?

    如果是在模板参数列表中,那么二者毫无区别。

    但是如果表示模板类型的成员是类型(模板的类型成员),那么就必须使用typename;

  2. inline与template

    首先,从语法上来说,如果要使用template + inline , 需要把inline放在template之后。

     template <typename T>
     inline
     void F(const T& t)
     {
         // ...
     }
    

    不过,对于非全特化的模板,inline关键字从实际效果上来看似乎毫无作用的——与inline函数一样,必须保证声明定义在一个文件;是否内联,有编译器决定。(不能完全肯定!看了爆栈上的讨论,没有看太清楚,但是有一点比较直白:如果你觉得这个函数该内联,那么管他什么呢,写上inline就行!)

  3. 显示实例化一个vector<NoDefault>(其中NoDefault类无默认构造函数)将会导致错误。

    因为其中的构造函数

     vector(size_type size,
            const T& value = T(),
            const Allocator alloc = Allocator())
    

    会因为尝试T()而错误。

    绕过这些操作,使用合理的非显示实例化就不会有问题。这有点像非编译语言(如Python),只有遇到没有错误时才报错(当然还是不同的,C++还是是在编译阶段报错。只能说,模板代码可能并不会全部被编译进二进制里)