41: 了解隐式接口与编译期多态

面向对象中的类设计时需要考虑显式接口和运行时多态,而模板编程中需要考虑隐式接口和编译器多态

  • 如果函数的形参是普通类:
    • 普通类的显式接口由函数签名(函数名、形参类型、返回值类型)表征,运行时多态由虚函数实现
    • 在函数进行编译时,就可以知道该普通类有哪些接口
  • 如果函数的形参是模板类型:
    • 模板类型的隐式接口由表达式的合法性表征(即该模板类型应该支持函数中形参调用的方法),编译器多态由模板初始化和重载函数的解析实现
    • 在函数进行编译时,无法知道模板类型有哪些接口,因此视为鸭子类型(即传入对象支持函数中调用的方法即可)
      • 在编译函数时当然无法确定模板类型,但是当传入实参后,内部如果调用了实参未定义的函数,同样会在编译期报错而非运行期
  • 参考:

42: 了解typename的双重意义

  • 在模板声明中,使用classtypename完全相同
  • 在模板内部,typename还可以用来显式指明【嵌套从属类型名称】
    • 背景:比如编译器无法在模板内部判断T::mem是一个static成员(默认),还是一个类型
    • 嵌套从属类型名称:T::mem是一个依赖于模板参数T的类型
    • 例子:模板内部typename T::age myAge = 25;
    • typename还可以用来显式指明【嵌套从属类型名称】,可以出现在模板内部、函数形参列表,但是不可以出现在【类派生列表】和【构造函数中成员初始化列表】中
      • 当类型名称过于复杂时,可以使用类型别名
  • 参考

43: 使用模板化基类中的成员函数

  • 背景:如果基类是一个模板类,派生类进行继承
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    class Buff {};
    class RedBuff: public Buff {};
    class BlueBuff: public Buff {};
    
    template <typename T>  // 基类
    class Container { /* 假设有成员函数func() */};
    
    //template <> // 全特化基类
    //class Container<Buff*> { /* 假设没有成员函数func() */ };
    // template <typename T=Buff*> // 使用默认模板实参,同全特化基类
    // class Container<T> { /* 假设没有成员函数func() */ }
    
    template <typename T>
    class PlayerContainer: public Container<T> { // 派生类继承模板化基类
        public:
            void test() { func(); } // 这里编译报错
    }; 
    
    • 对于模板化基类,可能有特化版本,且其中可能有不同的接口
    • 对于派生类而言,也无法确定类型T,因此C++规定派生类不在模板化基类中查找继承而来的接口
  • 解决方法:向编译器承诺所有的特化版本都遵循模板化基类的接口(或者说使用非特化版本的模板化基类中的接口)
    • 使用this显式指出访问基类的成员函数
      1
      2
      3
      4
      5
      
      template <typename T>
      class PlayerContainer: public Container<T> { // 派生类继承模板化基类
          public:
              void test() { this->func(); } // this指针可以访问所有成员函数
      }; 
      
    • 使用using声明
      1
      2
      3
      4
      5
      6
      
      template <typename T>
      class PlayerContainer: public Container<T> { // 派生类继承模板化基类
          public:
              using Container<T>::func; // 告诉编译器,func在模板化基类中
              void test() { func(); }
      }; 
      
    • 使用作用域运算符::明确指出,不推荐使用,因为如果func是虚函数,使用这种方法不会产生多态
      1
      2
      3
      4
      5
      
      template <typename T>
      class PlayerContainer: public Container<T> { // 派生类继承模板化基类
          public:
              void test() { Container<T>::func(); } // 明确指出
      }; 
      

44: 将与参数无关的代码抽离模板

  • 背景:代码膨胀
    • 模板提供的是编译期多态,不同的类型参数会生成不同的模板
    • 比如一个模板类接受一个类型参数T与一个非类型参数N,大部分成员都使用类型参数T,只有极少部分成员使用非类型参数N
    • 如果使用相同的类型type、但是不同的非类型参数n进行实例化,生成的代码中大部分都相同,只有极少部分不同
  • 抽取公共代码:
    • 模板中生成的冗余代码是隐式的,因为模板只有一份,生成不同实例后才可能产生冗余
    • 比如可以将与参数无关的代码(成员函数,数据成员)放入基类中,然后private继承
  • 参考

45: 运用成员函数模板接受所有兼容类型

  • 背景:假如类型参数T存在继承关系,但是模板实例化后是完全不同的两个类
    • 比如有一个继承体系,基类Base,派生类Derived
    • 指向派生类的指针可以转换为指向基类的指针:Base* p = new Derived();
    • 但是指向派生类的智能指针无法转换为指向基类的智能指针: shared_ptr<Base*> sp = make_shared<Derived*>(new Derived());
  • 重载构造函数
    • 接受同一模板的其他实例的构造函数称为通用构造函数
    • 兼容类型检查:将MySmartPtr<U>转换为MySmartPtr<T>,前提是类型U可以转换为类型T
    • 如果没有声明拷贝构造函数,编译器会自己生成一个,而非使用通用构造函数去进行成员模板实例化
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    template <typename T>
    class MySmartPtr{
        public:
            MyShartPtr(T* p): ptr(p) {}
            template <typename U>
            MySmartPtr(const MySmartPtr<U>& other): ptr(other.get()) {}; // 带类型兼容检查的通用构造函数,可以实现隐式类型转换(因为不带explicit)
            T* get() const {return ptr;}
        private:
            T *ptr;    
    };
    
    MySmartPtr<Derived*> dp(new Derived()); // 隐式类型转换
    MySmartPtr<Base*> bp = MySmartPtr<Derived*>(new Derived()); // T=Base*, U=Derived*
    
  • 参考

46:需要类型转换时请将模板定义为非成员函数

  • 背景:
    1
    2
    3
    4
    5
    6
    7
    8
    
    template <typename T>
    class Rational {};
    
    template <typename T>
    const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {}
    
    Rational<int> oneHalf(1,2);
    Rational<int> result = oneHalf * 2; // Error
    
  • 模板函数的调用过程:
    • 首先推导出类型T,将函数进行实例化:此时无法从2推导得出类型T
    • 在调用时,有的参数可能需要隐式类型转换
  • 解决方法:将模板函数定义为类的友元,因此类模板实例化后类型T已知
    • 如果仅仅是声明,编译器不会对友元函数进行实例化,因此需要进行定义
    • 定义在类内部的函数是inline的,可以在类外部定义一个辅助函数(也是模板函数,但是不需要隐式类型转换)
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    template <typename T> class Rational;
    template <typename T> const Rational<T> func(const Rational<T>& lhs, const Rational<T>& rhs) {}
    
    template <typename T>
    class Rational{
        public:
            friend Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs){
                func(lhs, rhs); // 可以推导出类型T,而且不需要进行隐式类型转换
            }
    }
    

47: 请使用traits classes表现类型信息

  • 使用Traits的特点:
    • 可以同时支持自定义类型和基础类型
    • 在编译期就获取信息

C++中的Traits类可以在编译期提供类型信息,是通过Traits模板及其特化来实现的 C++标准库中提供了不同的Traits:iterator_traits,char_traits,numeric_limits等(以iterator_traits为例)

  • 背景:容器与算法通过迭代器联系在一起,算法中可能需要知道迭代器的类型、迭代器中元素的类型,由此有不同的处理方法
    • 比如算法advance可以让一个迭代器移动n步(负数则反向移动)
      • 迭代器有五种:其中随机访问迭代器可以直接使用+=操作
        • C++提供了五个类标识迭代器类型:input_iterator_tagoutput_iterator_tagforward_iterator_tagbidirectional_iterator_tagrandom_access_iterator_tag
      • 传入的参数也可能是基本类型的指针
      1
      2
      3
      4
      5
      6
      7
      8
      
      template <typename IterT, typename DistT>
      void advance(IterT& iter, DistT d){
          // 判断迭代器类型
          if(iter is random access iterator) iter += d;
          else ...
          // 判断迭代器中元素类型
          if(iter.value_type is MyVector) cout<<"MyVector"<<endl;
      }
      
    • 分析:
      • 如果IterT是类类型,因此可以在类中携带数据成员,表示迭代器类型和元素类型
      • 但是IterT也可能是基本类型的指针类型,无法在其中携带信息
    • Traits技法:使用Traits可以通过一个模板类间接获取IterT的相关信息
      1
      2
      
      template <typename IterT>
      struct my_iterator_traits;
      
  • Traits是C++中一种编程惯例,允许在编译期得到类型的信息
    • traits是一个用来携带信息的很小的类,需要实现两个部分:
      • traits中的类型可能是用户自定义的类型,
        • 自定义类型中需要实现相应的迭代器,对具体的类型信息起一个通用的别名
        • traits中包装相应的信息
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        
        template <typename T>
        class MyVector{ // 自定义类
            public:
                class iterator{ // 自定义类中的迭代器
                    public:
                        using value_type = T;
                        using iterator_category = my_random_access_iterator_tag;
                };
        };
        
        // iterator_traits可以获取迭代器(或指针类型)的元素类型和迭代器类型(指针类型视为随机访问迭代器)
        template <typename IterT>
        struct iterator_traits{ // IterT是类类型
            using iterator_category = typename IterT::iterator_category;
            using value_type = typename IterT::value_type;
        };
        
      • traits中的类型可能是基本数据类型,遵循相同的名称,包装一下相应的信息
        1
        2
        3
        4
        5
        
        template <typename IterT>
        struct iterator_traits<IterT*>{ // 特化版本:IterT是基本类型,IterT是基本类型的指针
            using iterator_category = my_random_access_iterator_tag; // 指针可以使用+=操作,因此视为随机访问迭代器
            using value_type = IterT;
        };
        
  • 使用
    • 不好的写法:使用typeid在运行时判断类型
      • 但是IterT类型在编译期就可以确定,对象iter的类型需要在运行时确定
      • 更严重的问题:静态类型检查(编译期必须确保所有源码都有效,即使是不会执行的源码) ^826df6
        • 比如即使迭代器不是my_random_access_iterator_tag类型,编译期也会进入if语句测试该迭代器是否支持+=运算,不支持的话编译报错
    1
    2
    3
    4
    5
    
    template <typename IterT, typename DistT>
    void advance(IterT& iter, DistT d){
        if( typeid(typename std::iterator_traits<IterT>::iterator_category) == typeid(my_random_access_iterator_tag) ) 
            iter += d;
    }
    
    • 推荐实现方法:根据不同的类型创建不同的重载方法(worker),然后在一个master函数中调用,依据traits类型进行重载调用
     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
    
    template <typename IterT, typename DistT>
    void advance(IterT& iter, DistT d){
        // 将IterT中迭代器类型和元素类型萃取出来
        std::cout<<typeid(typename my_iterator_traits<IterT>::iterator_category).name()<<std::endl;
        std::cout<<typeid(typename my_iterator_traits<IterT>::value_type).name()<<std::endl;
    
        // 错误使用:如果iter是指针类型,则IterT为基本类型,无iterator_category属性
        // std::cout<<typeid(IterT::iterator_category).name()<<std::endl;
    
        doAdvance(iter, d, typename my_iterator_traits<IterT>::iterator_category());
        // 最后默认初始化一个iterator_category的对象,进行重载匹配,调用对应的函数
    }
    
    // 随机访问迭代器版本
    template <typename IterT, typename DistT>
    void doAdvance(IterT& iter, DistT d, my_random_access_iterator_tag){
        iter += d;
    }
    
    // 前向迭代器版本
    template <typename IterT, typename DistT>
    void doAdvance(IterT& iter, DistT d, my_forward_iterator_tag){
        assert(d >= 0 && "d must be not less then 0");
        while(d--) ++iter;
    }
    
  • 测试代码
  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
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
#include <iostream>
#include <memory>
#include <list>
#include <cassert>
// using namespace std;

struct my_random_access_iterator_tag {
    // my_random_access_iterator_tag() { std::cout<<"my_random_access_iterator_tag ctor"<<std::endl; }
};

struct my_forward_iterator_tag{
    // my_forward_iterator_tag() {std::cout<<"my_forward_iterator_tag"<<std::endl;}
};

template <typename T>
class MyVector{ // 自定义类
    public:
        class iterator{ // 自定义类中的迭代器
            public:
                using value_type = T;
                using iterator_category = my_random_access_iterator_tag;
        };
};

// iterator_traits可以获取迭代器(或指针类型)的元素类型和迭代器类型(指针类型视为随机访问迭代器)
// 输入:使用类类型的迭代器或指针类型进行实例化
// 输出:萃取出元素类型和迭代器类型
template <typename IterT>
struct my_iterator_traits{ // IterT是类类型
    using iterator_category = typename IterT::iterator_category;
    using value_type = typename IterT::value_type;
};

template <typename IterT>
struct my_iterator_traits<IterT*>{ // 特化版本:IterT是基本类型,IterT是基本类型的指针
    using iterator_category = my_random_access_iterator_tag; // 指针可以使用+=操作,因此视为随机访问迭代器
    using value_type = IterT;
};

// std双向迭代器版本
template <typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag){
    if(d > 0)
        while(d--) ++iter;
    else 
        while(++d) --iter;
}

// std随机访问迭代器版本
template <typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag){
    iter += d;
}

// 自定义随机访问迭代器版本
template <typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, my_random_access_iterator_tag){
    iter += d;
}

// 自定义前向迭代器版本
template <typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, my_forward_iterator_tag){
    assert(d >= 0 && "d must be not less then 0");
    while(d--) ++iter;
}

template <typename IterT, typename DistT>
void myAdvance(IterT& iter, DistT d){
    // 将IterT中迭代器类型和元素类型萃取出来
    std::cout<<typeid(typename my_iterator_traits<IterT>::iterator_category).name()<<std::endl;
    std::cout<<typeid(typename my_iterator_traits<IterT>::value_type).name()<<std::endl;
    
    // 错误使用:如果iter是指针类型,则IterT为基本类型,无iterator_category属性
    // std::cout<<typeid(IterT::iterator_category).name()<<std::endl;
    // std::cout<< (typeid(typename my_iterator_traits<IterT>::iterator_category) == typeid(std::bidirectional_iterator_tag)) <<std::endl;
    
    // 不好的写法:
    // 静态类型检查,即使iter不是随机访问迭代器,也会进入if语句块内进行检查
    // if( typeid(typename my_iterator_traits<IterT>::iterator_category) == typeid(std::random_access_iterator_tag) )
    //     iter += d;

    doAdvance(iter, d, typename my_iterator_traits<IterT>::iterator_category()); 
    // 最后默认初始化一个iterator_category的对象,进行重载匹配,调用对应的函数
}


int main(){
    int a[10];
    for(int i = 0; i < 10; ++i) a[i] = i+1;

    int* p = &a[0];
    myAdvance(p, 2);
    std::cout<<*p<<std::endl;

    std::list<int> lst{1,2,3,4,5,6,7,8,9,10};
    std::list<int>::iterator it = lst.begin();
    myAdvance(it, 2);
    std::cout<<*it<<std::endl;

    return 0;
}

48: 认识模板元编程

  • 模板元编程(template metaprogramming,TMP):编写模板,执行于编译期,生成具象化的代码
    • 优点:可以将很多工作从运行期转移到编译期
      • 一些错误可以提前发现
      • 运行时更高效:可执行文件体积小,运行期短,内存需求少
      • 避免了[[ch07-模板与泛型编程#^826df6|静态类型检查]]的问题
    • 缺点:编译时间变长
  • 模板元编程
    • 图灵完备
    • 循环由递归实现