7. 模板与泛型编程

28 阅读8分钟

小结

  1. 模板参数有类型参数非类型参数,类型参数是有隐式接口限制
  2. 模板会在编译期根据代码进行具象化,比如代码中确实有SmartPtr<int>,才会将T=int的代码写入源码中,但是同时引发的问题是代码膨胀,所以一般也需要将与参数无关代码抽离
  3. typename还需用来标识嵌套从属类型,但是在基类列表和初始化列表中不需要
  4. 模板类之间的继承,子类会默认隐藏父类所有接口,需要特殊手段才能调用父类函数
  5. 泛化构造、赋值操作,能实现类型转换,但是这并不会阻止编译器生成默认的几个函数
  6. 当调用模板函数(可能是成员)时,必须确保此函数已经被具象化,进而才会进行类型转换,所以尽量将此等函数写成friend
  7. traits class存储类型信息相关的信息
  8. 模板元编程运行在编译期,输出的是具象化后的源码,可能直接输出编译期常量

41. 了解隐式接口和编译期多态

  1. 显示接口:在源码中找到这个接口,比如class声明实现
  2. 隐式接口:由有效表达式组成,表达式会限制T类型必须支持哪些操作,如下,
template<class T>
void doSome(T& w){
    if(w.size()>10 && w != otherT){ //这里要求T类型必须支持 size() 调用以及 !=
        ...
    }
}
  1. 编译期多态:template会在编译期具象化,生成doSome<T>函数,调用也是调用这个函数
  2. template参数而言,接口是隐式的。静态多态则是通过template具象化或函数重载发生于编译期。

42. 了解typename的双重意义

  1. 在声明template类型参数时,typename与class没有区别
  2. 使用typename标识嵌套从属类型 (T::value_type,从属指与类型参数T有关,嵌套指某类内部定义的类型),为了区分value_type是T的static成员变量还是内置类型;但不得在基类列表和成员初始化列表中添加typename

嵌套从属类型还有可能是OtherType<T>::type,因为OtherType有可能特化,让type成为静态成员变量,而不是类型,所以需要使用typename显示指定

template <typename T>
void foo() {
    typename T::SubType* ptr;  // 必须加 typename,否则编译错误
}
  1. 特别如果想取别名,也必须在嵌套从属类型前加上typename进行标识,using Type = typename T::value_type;

43. 学习处理模板化基类内的名称

  1. 模板类继承时,基类成员在派生类模板中无法直接访问,因为基类可能特例化,导致成员不同,所以cpp索性就默认不让派生类去访问基类成员
  2. 解决方案:(本质都是明确告诉编译器这些接口都在基类存在)
  • this->baseFunc(); // 通过 this 指针访问,编译器会延迟检查到实例化时
  • using Base<T>::baseFunc; // 引入基类成员
  • Base<T>::baseFunc(); //(注意:若 baseFunc 是虚函数,可能会屏蔽多态性)
  1. 这是 C++ 模板“两阶段编译”(解析模板定义 → 实例化具体类型)的必然结果,目的是确保类型安全和早期错误诊断(即在定义时假设它对那些 base classes 的内容毫无所悉)。

44. 将与参数无关的代码抽离

  1. template可能会导致代码膨胀,需要设想多种类型来找出其中公共代码,抽象出函数或基类
  2. 非类型参数造成的代码膨胀,可以函数参数或class成员变量替换template参数
  3. 类型参数造成的代码膨胀(例如 vector<int> 和 vector<long> 的底层实现几乎相同,但编译器会生成两份独立代码),暂时不懂

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

  1. 运用成员函数模板接受所有兼容类型,需要兼容的场景有隐式转换、其他类型赋值
  2. 模板类的隐式转换涉及
  • 底层指针类型的转换SmartPtr<Base> ← SmartPtr<Derived>
  • 包装器层面的转换SmartPtr<Derived> ← unique_ptr<Derived>
  1. 底层指针类型转换的实现借助“泛化copy构造”或“泛化assignment操作”以及原始指针,将other底层指针赋值给当前对象指针
  2. 包装器层面的转换需要借助“泛化copy构造”或“泛化assignment操作”来指定具体其他模板类型,且要添加explicit
  3. 成员函数模板并不会影响阻止编译器生成默认的几个构造、赋值函数,所以还需要单独定义
    template <typename T>
    class SmartPtr {
    public:
        // 核心构造/析构
        SmartPtr() noexcept = default;
        ~SmartPtr() { delete heldPtr; }
    
        // 泛化拷贝构造(允许向上转型)
        template <typename U>
        SmartPtr(const SmartPtr<U>& other) 
            : heldPtr(other.get()) {
            static_assert(std::is_convertible_v<U*, T*>, 
                "Incompatible pointer types");
        }
    
        // 从unique_ptr构造(显式转移所有权)
        template <typename U>
        explicit SmartPtr(std::unique_ptr<U>&& other)
            : heldPtr(other.release()) {
            static_assert(std::is_convertible_v<U*, T*>,
                "Cannot convert pointer types");
        }
    
        // 必须显式声明特殊成员函数
        SmartPtr(const SmartPtr&) = default;
        SmartPtr(SmartPtr&&) noexcept = default;
        SmartPtr& operator=(const SmartPtr&) = default;
        SmartPtr& operator=(SmartPtr&&) noexcept = default;
    
        // 接口
        T* get() const noexcept { return heldPtr; }
    
    private:
        T* heldPtr = nullptr;
    };
    

模板类内部使用类名时,可以直接使用SmartPtr,且构造函数就是这个名字,后面不能加类型;而类外使用时,则需要在后面加类型SmartPtr<T>

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

  1. 模板函数与隐式转换的关系
  • 编译器需要先看到函数声明,才能尝试参数类型匹配和隐式转换
  • 对于模板函数,只有当代码中实际使用时才会具现化(生成具体类型的函数)
  • 如果需要的模板特化版本未被具现化,对应的函数就不存在,自然无法进行类型转换
  1. 支持混合类型运算的实现方法
  • 当运算符左右操作数类型不同时(如int * Rational
  • 需要通过友元函数在类内部定义运算符重载
  • 这样能确保: a) 函数随模板类一起具现化 b) 可以访问类的私有成员 c) 保持inline优化优势
// 前向声明
template<typename T> class Rational;
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs);

// 模板类定义
template<typename T>
class Rational {
public:
    // 模板友元函数(推荐写法)
    friend const Rational operator*(const Rational& lhs, const Rational& rhs) {// Rational不加T是因为编译器会自动加
        return doMultiply(lhs, rhs);// 这里不加T是因为自动类型推导
    }
};

// 实现 doMultiply
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs) {
    // 具体乘法实现
    return Rational<T>(...);
}

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

  1. traits允许在编译期间取得某些类型信息,比如int类型的最大最小值
  2. 因为对内置类型也是需要这些类型信息,而我们不能对内置类型内部添加成员,故只能将这些类型信息放置在类外,也就是本章的traits class
  3. 以实现iterator_traits::iterator_category为例
  • 迭代器本身有很多类型,如forward_iterator、bidirectional_iterator_tag、random_access_iterator_tag等,每种对于移动不同,第一种只能向后,第二种可前后移动,第三种可随机访问,类似于指针
  • 在不同容器中,可能iterator类型也是不同的,所以定义了一个统一的移动方法advance(Iter& iter,DistT d),内部需要判断是哪种指针,进而处理;如果是运行时检查,可以使用if-else,但是如果需要在编译时检测,则借助函数重载,传入不同类型进而调用不同函数
  • 问题来到advance函数如果获得类型信息iterator_category,需要外部定义iterator_traits,同时iterator本身也需要将自己类型暴露出来,让iterator_traits获得,进而再暴露出去
  • 此处又有特例:指针,需要一个偏特化,指针类型为random_access_iterator_tag
template<...>
class deque{//deque内部的iterator
public:
    class iterator{
    public:
        // 需要在此处暴露出自己是哪种类型
        using iterator_category = random_access_iterator_tag;
        ...
    };
    ...
};

template<class IterT>
struct iterator_traits{ // 一般都是用class
public:
    // 获取迭代器类型,再暴露出去
    using iterator_category =typename IterT::iterator_category;
    ...
};

template<class T>
struct iterator_traits<T*>{ // 偏特化,专门针对指针类型
    using iterator_category = random_access_iterator_tag;
};
  1. 类型标签分发:定义空结构体作为标签struct random_access_iterator_tag {};,结合函数重载,在编译期根据类型选择不同的实现路径
// 双向迭代器:可前进或后退
template <typename Iter>
void advance_impl(Iter& it, int n, bidirectional_iterator_tag) { // 这里直接省略变量名
    if (n > 0) while (n--) ++it;
    else while (n++) --it;
}

// 随机访问迭代器:直接跳跃
template <typename Iter>
void advance_impl(Iter& it, int n, random_access_iterator_tag) {
    it += n;
}

template <typename Iter>
void advance(Iter& it, int n) {
    // 获取迭代器类别标签(假设 Iter 定义了 iterator_category)
    using tag = typename Iter::iterator_category;
    // 分发到对应的重载
    advance_impl(it, n, tag{}); // tag{}是创建临时变量
}

如果函数参数在函数体内确实未被使用,可以省略参数名

48. 认识模板元编程

  1. 在编译期运行,会在具象化代码部分被替换成运行出来的代码
  2. 模板元编程(Template Metaprogramming, TMP)的最终输出是具体化的(具象化的)代码,最终可能是常量、类型或函数实现
template<int N>
struct Factorial {
    static const int value = N * Factorial<N-1>::value;  // 递归展开
};

template<>
struct Factorial<0> {
    static const int value = 1;  // 递归终止条件
};

int arr[Factorial<5>::value]; // => int arr[120];