小结
- 模板参数有类型参数和非类型参数,类型参数是有隐式接口限制
- 模板会在编译期根据代码进行具象化,比如代码中确实有
SmartPtr<int>,才会将T=int的代码写入源码中,但是同时引发的问题是代码膨胀,所以一般也需要将与参数无关代码抽离 - typename还需用来标识嵌套从属类型,但是在基类列表和初始化列表中不需要
- 模板类之间的继承,子类会默认隐藏父类所有接口,需要特殊手段才能调用父类函数
- 泛化构造、赋值操作,能实现类型转换,但是这并不会阻止编译器生成默认的几个函数
- 当调用模板函数(可能是成员)时,必须确保此函数已经被具象化,进而才会进行类型转换,所以尽量将此等函数写成friend
- traits class存储类型信息相关的信息
- 模板元编程运行在编译期,输出的是具象化后的源码,可能直接输出编译期常量
41. 了解隐式接口和编译期多态
- 显示接口:在源码中找到这个接口,比如class声明实现
- 隐式接口:由有效表达式组成,表达式会限制T类型必须支持哪些操作,如下,
template<class T>
void doSome(T& w){
if(w.size()>10 && w != otherT){ //这里要求T类型必须支持 size() 调用以及 !=
...
}
}
- 编译期多态:template会在编译期具象化,生成
doSome<T>函数,调用也是调用这个函数 - template参数而言,接口是隐式的。静态多态则是通过template具象化或函数重载发生于编译期。
42. 了解typename的双重意义
- 在声明template类型参数时,typename与class没有区别
- 使用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,否则编译错误
}
- 特别如果想取别名,也必须在嵌套从属类型前加上typename进行标识,
using Type = typename T::value_type;
43. 学习处理模板化基类内的名称
- 模板类继承时,基类成员在派生类模板中无法直接访问,因为基类可能特例化,导致成员不同,所以cpp索性就默认不让派生类去访问基类成员
- 解决方案:(本质都是明确告诉编译器这些接口都在基类存在)
this->baseFunc();// 通过 this 指针访问,编译器会延迟检查到实例化时using Base<T>::baseFunc;// 引入基类成员Base<T>::baseFunc();//(注意:若 baseFunc 是虚函数,可能会屏蔽多态性)
- 这是 C++ 模板“两阶段编译”(解析模板定义 → 实例化具体类型)的必然结果,目的是确保类型安全和早期错误诊断(即在定义时假设它对那些 base classes 的内容毫无所悉)。
44. 将与参数无关的代码抽离
- template可能会导致代码膨胀,需要设想多种类型来找出其中公共代码,抽象出函数或基类
- 因非类型参数造成的代码膨胀,可以函数参数或class成员变量替换template参数
- 类型参数造成的代码膨胀(例如
vector<int>和vector<long>的底层实现几乎相同,但编译器会生成两份独立代码),暂时不懂
45. 运用成员函数模板接受所有兼容类型
- 运用成员函数模板接受所有兼容类型,需要兼容的场景有隐式转换、其他类型赋值
- 模板类的隐式转换涉及
- 底层指针类型的转换(
SmartPtr<Base>←SmartPtr<Derived>) - 包装器层面的转换(
SmartPtr<Derived>←unique_ptr<Derived>)
- 底层指针类型转换的实现借助“泛化copy构造”或“泛化assignment操作”以及原始指针,将other底层指针赋值给当前对象指针
- 包装器层面的转换需要借助“泛化copy构造”或“泛化assignment操作”来指定具体其他模板类型,且要添加explicit
- 成员函数模板并不会影响阻止编译器生成默认的几个构造、赋值函数,所以还需要单独定义
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. 需要类型转换时请为模板定义非成员函数
- 模板函数与隐式转换的关系
- 编译器需要先看到函数声明,才能尝试参数类型匹配和隐式转换
- 对于模板函数,只有当代码中实际使用时才会具现化(生成具体类型的函数)
- 如果需要的模板特化版本未被具现化,对应的函数就不存在,自然无法进行类型转换
- 支持混合类型运算的实现方法
- 当运算符左右操作数类型不同时(如
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表现类型信息
- traits允许在编译期间取得某些类型信息,比如int类型的最大最小值
- 因为对内置类型也是需要这些类型信息,而我们不能对内置类型内部添加成员,故只能将这些类型信息放置在类外,也就是本章的
traits class - 以实现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;
};
- 类型标签分发:定义空结构体作为标签
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. 认识模板元编程
- 在编译期运行,会在具象化代码部分被替换成运行出来的代码
- 模板元编程(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];