带你读懂Effective C++(七): 模板和泛型编程
本文系统阐述了C++模板编程的8个核心条款(41-48),重点解析了隐式接口与编译期多态(条款41)、typename的双重意义(条款42)、模板化基类名称处理(条款43)、参数无关代码抽离(条款44)、成员函数模板兼容性(条款45)、非成员函数类型转换(条款46)、traits类类型信息(条款47)以及模板元编程(条款48)。这些条款揭示了模板编程的底层机制:通过隐式接口和编译期多态实现泛型编程
目录
条款 47:请使用 traits classes 表现类型信息
条款 41:了解隐式接口和编译期多态
核心解释
C++ 中类(class)和模板(template)均支持接口和多态两大核心特性,但实现方式、生效阶段完全不同:面向对象编程(OOP)基于显式接口和运行期多态,以函数签名为核心、通过 virtual 函数实现;模板与泛型编程则基于隐式接口和编译期多态,以有效表达式为核心、通过模板具现化和函数重载解析实现。隐式接口是模板编程的核心特征,其约束并非来自显式的函数签名,而是模板代码中对参数类型的有效操作表达式。
关键对比与示例
1. 面向对象:显式接口 + 运行期多态
显式接口:类的公有函数签名(返回值、参数、名称)明确构成接口,编译器编译期严格检查是否实现对应签名;运行期多态:通过 virtual 函数实现,函数调用版本在运行期根据对象实际类型确定。
#include <string>
using namespace std;
// 显式接口:由公有函数签名构成(name()、id())
class Person {
public:
virtual string name() const = 0;
virtual int id() const = 0;
};
// 严格遵循显式接口:必须实现所有纯虚函数签名
class Student : public Person {
public:
string name() const override { return "student"; }
int id() const override { return 1; }
};
// 编译期检查:p必须是Person派生类,实现name()/id()显式接口
void printPerson(const Person& p) {
cout << p.name() << ":" << p.id() << endl; // 运行期多态:调用实际对象的版本
}
2. 模板:隐式接口 + 编译期多态
隐式接口:模板参数的约束并非显式签名,而是模板代码中对参数的有效表达式(如支持.访问、()调用、运算符重载等),编译器编译期检查表达式是否有效;编译期多态:通过模板具现化(不同参数生成不同代码)和函数重载解析实现,函数调用版本在编译期根据参数声明类型确定。
#include <iostream>
using namespace std;
// 隐式接口:对T的约束是「支持调用name()、id(),且返回值可被cout输出」
template <typename T>
void printObj(const T& obj) {
cout << obj.name() << ":" << obj.id() << endl; // 有效表达式构成隐式接口
}
// 任意类型,只要满足隐式接口即可作为模板参数(无需继承、无需显式声明)
class Teacher {
public:
string name() const { return "teacher"; }
int id() const { return 2; }
};
class Book {
public:
string name() const { return "C++ Primer"; }
int id() const { return 100; }
};
// 编译期多态:根据T的类型具现化出printObj<Teacher>和printObj<Book>
int main() {
Teacher t;
Book b;
printObj(t); // 具现化printObj<Teacher>
printObj(b); // 具现化printObj<Book>
return 0;
}
核心要点
- 显式接口是签名式接口,由类的公有成员函数签名明确定义,约束 “类必须实现什么”;隐式接口是表达式接口,由模板代码中的有效操作定义,约束 “类型支持什么操作”;
- 运行期多态的核心是动态绑定,依赖 virtual 函数和对象实际类型,有轻微运行期开销;编译期多态的核心是模板具现化 / 重载解析,编译期生成专属代码,无运行期开销;
- 隐式接口更灵活,支持非面向对象类型(如内置类型、无继承关系的自定义类型),是泛型编程的基础。
请记住
class 和 template 都支持接口和多态;对 class 而言接口是显式的,以函数签名为中心,多态则是通过 virtual 函数发生于运行期;对 template 参数而言,接口是隐式的,奠基于有效表达式,多态则主要通过 template 具现化和函数重载解析发生于编译期。
条款 42:了解 typename 的双重意义
核心解释
C++ 中typename关键字仅有两个合法且明确的用途,二者适用场景严格区分,不可混淆:一是声明模板参数时作为前缀,与class关键字可互换;二是修饰嵌套从属类型名称,明确告知编译器该名称是 “类型” 而非 “静态成员 / 变量”,这是模板编程中的必用场景。同时typename有严格的使用限制:不可在基类列表、成员初值列中作为基类修饰符。
关键概念:嵌套从属类型名称
指 “依赖于模板参数的类型,且该类型嵌套在另一个类 / 模板中”(如T::const_iterator),编译器编译模板时,未具现化前无法确定该名称是类型还是静态成员,需typename显式标识。
关键用法与禁用场景示例
用法 1:声明模板参数,与 class 互换
模板参数声明时,template <typename T>和template <class T>完全等价,无任何功能差异,仅编码习惯不同。
// 两种写法等价,均声明模板参数T
template <typename T> T add(T a, T b) { return a + b; }
template <class T> T mul(T a, T b) { return a * b; }
用法 2:修饰嵌套从属类型名称(必用)
模板内使用嵌套从属类型时,必须加typename,否则编译器会将该名称解析为 “静态成员变量”,导致编译错误。
#include <vector>
using namespace std;
template <typename T>
void traverse(const T& container) {
// 正确:typename标识T::const_iterator是类型(嵌套从属类型)
typename T::const_iterator it = container.begin();
// 错误:无typename,编译器认为T::const_iterator是静态成员
// T::const_iterator it = container.begin();
}
// 具现化时,T为vector<int>,T::const_iterator是明确的迭代器类型
int main() {
vector<int> v = {1,2,3};
traverse(v);
return 0;
}
禁用场景:基类列表 + 成员初值列
这两个场景中,编译器可明确解析嵌套从属类型为基类,无需typename,且标准 C++ 禁止在此处使用typename。
template <typename T>
class Base {
public:
class Nested { // 嵌套类型,供派生类继承
explicit Nested(int x) {}
};
};
template <typename T>
// 禁用:基类列表中不可加typename
class Derived : public Base<T>::Nested {
public:
// 禁用:成员初值列中不可加typename
explicit Derived(int x) : Base<T>::Nested(x) {
// 允许:函数体内部使用嵌套从属类型,需加typename
typename Base<T>::Nested temp;
}
};
核心要点
- 模板参数声明时,
typename和class的互换性仅适用于类型参数,非类型参数(如size_t N)不可用typename声明; - 嵌套从属类型名称是模板编程的 “语法陷阱”,只要模板内使用 “
模板参数::嵌套名称” 且该名称是类型,必须加typename; typename的核心作用是向编译器提供类型提示,解决模板编译期的 “名称解析歧义”。
请记住
声明 template 参数时,前缀关键字 class 和 typename 可互换;请使用关键词 typename 标识嵌套从属类型名称,但不得在 base class lists(基类列)或 member initialization list 内以它为 base class 修饰符。
条款 43:学习处理模板化基类内的名称
核心解释
继承模板化基类(如Derived<Company>继承MsgSender<Company>)的派生类模板中,编译器不会默认解析基类模板的成员名称(函数 / 变量),直接调用会导致编译错误。根源是编译器编译派生类模板时,未具现化前无法确定基类模板是否有该成员(基类模板可能被特化,特化版本可能无此成员),因此会屏蔽基类模板的成员名称。需通过三种显式方式告知编译器该名称来自基类模板,其中前两种保留多态特性,第三种会丢失。
关键错误示例与解决方法
错误示例:直接调用模板化基类的成员
派生类模板LoggingMsgSender直接调用基类模板MsgSender的sendCleartext,编译器因无法解析基类成员名称而报错。
#include <string>
using namespace std;
class CompanyA { public: void sendCleartext(const string&) {} };
class CompanyB { public: void sendCleartext(const string&) {} };
struct MsgInfo {};
// 基类模板:消息发送器
template <class Company>
class MsgSender {
public:
void sendCleartext(const MsgInfo& info) {
Company c;
c.sendCleartext("msg");
}
void sendEncrypted(const MsgInfo& info);
};
// 派生类模板:带日志的消息发送器
template <class Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void sendCleartext(const MsgInfo& info) {
// 错误:编译器无法解析sendCleartext,认为是未定义名称
sendCleartext(info);
}
};
解决方法 1:this-> 前缀(推荐,保留多态)
在派生类模板中,this->会明确指向基类模板的成员,编译器会在模板具现化时解析该成员,且保留 virtual 函数的动态绑定特性。
template <class Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void sendCleartext(const MsgInfo& info) {
this->sendCleartext(info); // 正确:通过this->指向基类成员
}
};
解决方法 2:using 声明式(推荐,保留多态)
通过using MsgSender<Company>::成员名,将基类模板的成员名称引入派生类作用域,编译器可直接解析,同样保留多态特性,适合需要多次调用基类成员的场景。
template <class Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
using MsgSender<Company>::sendCleartext; // 引入基类成员名称
void sendCleartext(const MsgInfo& info) {
sendCleartext(info); // 正确:作用域内已有该名称
}
};
解决方法 3:显式基类资格修饰符(不推荐,丢失多态)
通过MsgSender<Company>::sendCleartext直接调用基类成员,虽能编译,但会强制静态绑定,若基类成员是 virtual 函数,会丢失动态绑定特性,仅适用于非虚成员。
template <class Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void sendCleartext(const MsgInfo& info) {
// 正确但不推荐:静态绑定,丢失virtual多态
MsgSender<Company>::sendCleartext(info);
}
};
核心要点
- 该问题仅存在于模板化基类的派生类模板中,非模板基类 / 非模板派生类无此问题(编译器可直接解析基类成员);
- 编译器屏蔽基类模板成员的根本目的是避免模板特化导致的成员缺失错误(如基类模板的特化版本无该成员,直接调用会引发未定义行为);
- 优先使用
this->或using声明式,二者均能保留多态特性,符合面向对象设计。
请记住
可在 derived class templates 内通过 this-> 指涉 base class templates 内的成员函数,或由一个明显写出 base class 资格修饰符完成。
条款 44:将与参数无关的代码抽离 templates
核心解释
模板的本质是代码生成器,class template 和 function template 会根据不同参数具现化出不同的类 / 函数,若模板代码中包含与参数无关的逻辑,会导致代码膨胀(相同逻辑被多次生成,增加可执行文件体积、降低缓存命中率)。解决核心是共性与变性分析:将模板中与参数无关的共性代码抽离到独立的非模板类 / 函数中,模板仅保留与参数相关的变性逻辑,通过继承或复合复用共性代码,其中非类型模板参数(如size_t N)是代码膨胀的主要诱因,抽离效果最显著。
关键代码膨胀示例与优化方案
错误示例:含非类型参数的模板导致代码膨胀
矩阵类Matrix<T, N>的invert(求逆)逻辑仅与类型T相关,与矩阵维度N(非类型参数)无关,但未抽离时,不同N会具现化出完全相同的invert代码,造成严重膨胀。
// 问题模板:T(类型参数)、N(非类型参数)
template<class T, size_t N>
class Matrix {
public:
void invert() { // 求逆逻辑:仅与T相关,与N无关
// 相同的求逆代码,会因N不同被多次生成(如N=2、3、4各一份)
cout << "Matrix<" << typeid(T).name() << "," << N << "> invert" << endl;
}
};
// 具现化:Matrix<int,2>、Matrix<int,3>的invert代码完全相同,却被生成两次
Matrix<int, 2> m2;
Matrix<int, 3> m3;
m2.invert();
m3.invert();
优化方案:抽离共性代码到非模板基类
将与非类型参数N无关、仅与类型参数T相关的代码抽离到独立的MatrixBase<T>中,原模板Matrix<T, N>通过private 继承复用基类逻辑(private 继承表示 “实现复用”,非 is-a 关系),仅将N作为参数传递给基类方法,彻底消除代码膨胀。
// 共性基类:仅含类型参数T,抽离所有与N无关的代码
template <class T>
class MatrixBase {
public:
// 接收N作为函数参数,替代原模板的非类型参数
void invert(size_t N) {
// 仅生成一份与T相关的代码(如int类型仅一份),所有N共享
cout << "MatrixBase<" << typeid(T).name() << "> invert, N=" << N << endl;
}
};
// 优化后模板:private继承MatrixBase,仅保留与N相关的逻辑
template <class T, size_t N>
class Matrix : private MatrixBase<T> {
public:
void invert() {
this->invert(N); // 调用基类方法,传递N参数
}
};
// 具现化:Matrix<int,2>、Matrix<int,3>共享MatrixBase<int>::invert,无代码膨胀
Matrix<int, 2> m2;
Matrix<int, 3> m3;
m2.invert(); // 调用MatrixBase<int>::invert(2)
m3.invert(); // 调用MatrixBase<int>::invert(3)
核心要点
- 代码膨胀分两种类型:非类型参数膨胀(易消除,用函数参数 / 成员变量替代模板参数)、类型参数膨胀(可降低,让二进制表示相同的类型共享实现,如
int和long若二进制相同,可共享代码); - 模板抽离的核心原则:任何模板代码都不应与造成膨胀的模板参数产生依赖;
- 采用 private 继承而非 public 继承,因基类仅为派生类提供实现复用,二者无面向对象的 is-a 语义,符合 “组合 / 继承的语义区分” 原则。
请记住
template 生成多个 class 和多个函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系;因非类型模板参数 (non-type template parameters) 而造成的代码膨胀,往往可以消除,做法是以函数参数或 class 成员变量替换 template 参数;因类型参数 (type parameters) 而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表示的具现类型共享实现码。
条款 45:运用成员函数模板接受所有兼容类型
核心解释
成员函数模板是指定义在类 / 类模板内部的模板函数,其核心作用是为类生成泛化的成员函数,支持接受所有类型兼容的参数(如派生类指针赋值给基类智能指针、不同类型的容器互相拷贝),是实现 “泛型兼容” 的关键技术。最典型的应用是 C++ 标准库的智能指针(如std::shared_ptr/std::unique_ptr),通过成员函数模板实现 “派生类指针隐式转换为基类指针” 的泛型构造。需注意:成员函数模板不会替代类的默认特殊成员函数(如拷贝构造、赋值运算符),若需要仍需显式声明。
关键应用示例:std::shared_ptr 的泛型构造
智能指针shared_ptr<T>需要支持 “用任意派生类U*构造shared_ptr<T>”(只要U继承T),通过成员函数模板实现该泛型兼容,无需为每个派生类编写单独的构造函数。
#include <iostream>
#include <memory>
using namespace std;
// 基类与派生类
class Base { public: virtual ~Base() { cout << "Base destruct" << endl; } };
class Derived : public Base { public: ~Derived() { cout << "Derived destruct" << endl; } };
// 模拟std::shared_ptr的成员函数模板实现
template <class T>
class MySharedPtr {
private:
T* ptr;
public:
// 普通构造函数:接受T*
explicit MySharedPtr(T* p) : ptr(p) { cout << "MySharedPtr<T> construct" << endl; }
// 成员函数模板:泛型构造,接受所有兼容的U*(U继承T)
template <class U>
explicit MySharedPtr(U* p) : ptr(p) { cout << "MySharedPtr<U> construct" << endl; }
// 析构函数
~MySharedPtr() { delete ptr; }
};
// 用法:派生类指针直接构造基类智能指针,实现泛型兼容
int main() {
// 正确:成员函数模板具现化MySharedPtr<Derived*>,转换为MySharedPtr<Base>
MySharedPtr<Base> p(new Derived());
return 0;
}
关键注意点:成员函数模板不替代默认特殊成员函数
编译器不会因成员函数模板而生成默认的拷贝构造、赋值运算符,若需要支持 “泛型拷贝 / 赋值” 同时保留 “普通拷贝 / 赋值”,需同时声明成员函数模板和普通特殊成员函数。
template <class T>
class MySharedPtr {
public:
// 普通拷贝构造:支持同类型拷贝
MySharedPtr(const MySharedPtr<T>& other) { /* 实现 */ }
// 成员函数模板:泛型拷贝构造,支持兼容类型拷贝(如MySharedPtr<Derived> → MySharedPtr<Base>)
template <class U>
MySharedPtr(const MySharedPtr<U>& other) { /* 实现 */ }
// 普通赋值运算符
MySharedPtr<T>& operator=(const MySharedPtr<T>& other) { /* 实现 */ }
// 成员函数模板:泛型赋值运算符
template <class U>
MySharedPtr<T>& operator=(const MySharedPtr<U>& other) { /* 实现 */ }
};
核心要点
- 成员函数模板的核心价值是泛型兼容,避免为每个兼容类型编写重复的成员函数,大幅提升类的灵活性;
- 兼容类型的判定依据是模板代码中的有效表达式(隐式接口),如
U*可转换为T*、U支持T的相关操作; - 成员函数模板是 “补充” 而非 “替代”,不会覆盖编译器生成的默认特殊成员函数,需显式声明普通特殊成员函数以保证基础功能。
请记住
请使用 member function template 生成 "可接受所有兼容类型" 的函数;如果你声明 member template 用于 "泛化 copy 构造" 或 "泛化 assignment 操作",你还是需要声明正常的 copy 构造函数和 copy assignment 操作符。
条款 46:需要类型转换时请为模板定义非成员函数
核心解释
当类模板的非成员函数(如运算符重载、工厂函数)需要支持所有参数的隐式类型转换时,必须将该函数定义为类模板内部的友元函数(且在类内完成实现)。根源是:外部的非成员模板函数会触发模板参数推导,编译器在推导过程中会屏蔽参数的隐式类型转换;而类内的友元函数不参与模板参数推导(模板参数由类模板确定),因此所有参数均可正常进行隐式类型转换,这是模板编程中实现 “双向隐式转换” 的唯一方法。
关键问题与解决示例
问题示例:外部非成员模板函数无法支持隐式类型转换
以数值类模板Num<T>的operator+为例,外部实现的非成员模板函数无法支持Num<int> + int的隐式转换(int无法转换为Num<int>),因模板参数推导屏蔽了隐式转换。
#include <iostream>
using namespace std;
// 数值类模板
template <class T>
class Num {
private:
T val;
public:
explicit Num(T v) : val(v) {} // 单参数构造,支持隐式类型转换
T getVal() const { return val; }
};
// 外部非成员模板函数:operator+
template <class T>
Num<T> operator+(const Num<T>& a, const Num<T>& b) {
return Num<T>(a.getVal() + b.getVal());
}
int main() {
Num<int> a(10);
// 错误:编译器推导模板参数时,不会将5隐式转换为Num<int>
// Num<int> c = a + 5;
return 0;
}
解决示例:类内定义友元非成员函数
将operator+定义为Num<T>内部的友元函数(且在类内实现),此时函数的模板参数由类模板T确定,不参与独立的模板参数推导,因此两个参数均可进行隐式类型转换,支持Num<int> + int、int + Num<int>等所有兼容形式。
#include <iostream>
using namespace std;
template <class T>
class Num {
private:
T val;
public:
Num(T v) : val(v) {} // 单参数构造,支持隐式类型转换
T getVal() const { return val; }
// 核心:类内定义友元非成员函数,无独立模板参数推导
friend Num<T> operator+(const Num<T>& a, const Num<T>& b) {
return Num<T>(a.val + b.val); // 可直接访问私有成员
}
};
int main() {
Num<int> a(10);
// 正确:5隐式转换为Num<int>,支持双向隐式类型转换
Num<int> c1 = a + 5;
Num<int> c2 = 5 + a;
cout << c1.getVal() << " " << c2.getVal() << endl; // 15 15
return 0;
}
核心要点
- 该规则仅适用于类模板的非成员函数,普通类的非成员函数可直接外部实现,无需定义为友元;
- 类内友元函数的关键优势:模板参数由类模板确定,跳过独立的模板参数推导,从而保留所有参数的隐式类型转换能力;
- 类内定义的友元函数会被编译器视为内联函数,适合实现简单的运算符重载、工具函数,无额外性能开销。
请记住
当我们编写一个 class template,而它提供之 "于此 template 相关的" 函数支持 "所有函数之隐式类型转换时",请将这些函数定义为 "class template" 内部的 friend 函数 (要在类里定义)。
条款 47:请使用 traits classes 表现类型信息
核心解释
Traits 类(特性类) 是 C++ 泛型编程的核心工具,其作用是让类型相关的信息在编译期可见,并为不同类型提供统一的信息访问接口。Traits 类完全通过模板特化实现:先定义基础的主模板,为通用类型提供默认的类型信息;再为特殊类型编写模板特化版本,定制专属的类型信息。结合函数重载技术后,Traits 类可实现编译期的类型判断(模拟编译期if...else),是 STL 迭代器、算法设计的基础(如std::iterator_traits、std::numeric_limits)。
关键应用:STL 迭代器的 traits 类
STL 为不同迭代器(输入、输出、随机访问等)定义了std::iterator_traits,通过该类可统一获取迭代器的关联类型(如值类型、指针类型),让算法无需关注迭代器的具体类型,实现泛型兼容。
关键实现步骤与示例
以自定义迭代器 Traits 类为例,拆解 Traits 类的三步实现法:定义主模板、模板特化、重载辅助函数实现编译期判断。
步骤 1:定义主模板,提供默认类型信息
主模板以迭代器类型为参数,定义统一的关联类型(如值类型value_type),为通用迭代器提供默认实现。
#include <iostream>
#include <vector>
using namespace std;
// 步骤1:主模板:为任意迭代器提供默认类型信息
template <typename Iter>
struct iterator_traits {
typedef typename Iter::value_type value_type; // 假设迭代器有嵌套value_type
};
// 偏特化:支持原生指针(原生指针无嵌套类型,需单独特化)
template <typename T>
struct iterator_traits<T*> {
typedef T value_type; // 原生指针T*的值类型为T
};
步骤 2:模板特化,为特殊类型定制信息
对无嵌套类型的特殊类型(如原生指针T*、const 指针const T*)编写偏特化 / 全特化版本,补充专属的类型信息,保证 Traits 类的通用性。
// 偏特化:支持const原生指针,值类型为T(而非const T)
template <typename T>
struct iterator_traits<const T*> {
typedef T value_type;
};
步骤 3:结合函数重载,实现编译期类型判断
通过重载辅助函数,根据 Traits 类提供的类型信息,为不同类型执行不同的逻辑,实现编译期的 “分支判断”,无运行期开销。
// 辅助函数:根据迭代器值类型执行不同逻辑
template <typename T>
void print_value_type(T) {
cout << "value_type: " << typeid(T).name() << endl;
}
// 泛型算法:利用traits类获取迭代器值类型,实现泛型兼容
template <typename Iter>
void algorithm(Iter it) {
// 编译期获取迭代器的value_type
typedef typename iterator_traits<Iter>::value_type ValType;
print_value_type(ValType());
}
// 测试:支持普通迭代器和原生指针
int main() {
vector<int> v;
algorithm(v.begin()); // 迭代器:value_type=int
int arr[5] = {1,2,3};
algorithm(arr); // 原生指针int*:value_type=int
const int c = 10;
algorithm(&c); // const指针const int*:value_type=int
return 0;
}
核心要点
- Traits 类的本质是编译期的类型信息容器,统一了不同类型的信息访问接口,让泛型算法无需关注类型细节;
- 模板特化是 Traits 类的实现基础,必须为无默认嵌套类型的特殊类型(如原生指针)编写特化版本;
- Traits 类结合函数重载,是实现编译期多态的重要手段,可替代运行期的类型判断,提升程序效率。
请记住
Traits classes 使得类型相关信息在编译器可见,它们以 template 和 template 特化完成实现;整合重载技术后,traits class 有可能在编译期对类型进行 if...else 测试。
条款 48:认识 template 元编程
核心解释
模板元编程(TMP,Template Metaprogramming) 是一种以 C++ 模板为工具,在编译期执行计算、代码生成、逻辑判断的编程范式,简单来说就是 “用代码生成代码”。TMP 将传统的运行期工作(如数值计算、类型判断、策略选择)移至编译期完成,核心优势是早期错误侦测(编译期发现逻辑错误)和更高的运行效率(编译期计算结果直接嵌入代码,无运行期开销)。TMP 可实现 “基于政策选择的代码组合”,还能避免为特殊类型生成不适合的代码,是标准库和高性能库的重要设计技术。
关键应用示例
示例 1:编译期数值计算(阶乘)
最经典的 TMP 示例,通过模板递归实现编译期阶乘计算,编译器直接生成计算结果,运行期无需任何计算。
#include <iostream>
using namespace std;
// TMP编译期阶乘:主模板(递归终止条件:N=0)
template <int N>
struct Factorial {
static const int value = N * Factorial<N-1>::value; // 模板递归
};
// 模板特化:递归终止条件(N=0,阶乘为1)
template <>
struct Factorial<0> {
static const int value = 1;
};
// 用法:编译期计算,运行期直接使用结果
int main() {
const int f5 = Factorial<5>::value; // 编译期计算:5!=120
const int f10 = Factorial<10>::value; // 编译期计算:10!=3628800
cout << f5 << " " << f10 << endl;
return 0;
}
示例 2:基于政策的代码组合(智能指针释放策略)
通过 TMP 结合策略模式,为智能指针定制不同的内存释放策略(普通删除、数组删除、自定义删除),编译期根据策略类型生成专属代码,实现 “策略可配置、无运行期开销”。
#include <iostream>
#include <type_traits>
using namespace std;
// 定义不同的释放策略(Policy)
struct NormalDelete { // 策略1:普通对象删除
template <typename T>
static void release(T* ptr) {
cout << "Normal delete: " << ptr << endl;
delete ptr;
}
};
struct ArrayDelete { // 策略2:数组删除
template <typename T>
static void release(T* ptr) {
cout << "Array delete[]: " << ptr << endl;
delete[] ptr;
}
};
struct CustomDelete { // 策略3:自定义删除
template <typename T, typename Fun = void(*)(T*)>
static void release(T* ptr, Fun func = [](T* p){ delete p; }) {
cout << "Custom delete: " << ptr << endl;
func(ptr);
}
};
// 智能指针模板:TMP组合释放策略,编译期确定策略类型
template <typename T, typename ReleasePolicy = NormalDelete>
class CustomSmartPtr {
private:
T* ptr;
public:
explicit CustomSmartPtr(T* p) : ptr(p) {}
// 析构函数:编译期根据策略类型选择释放方式
~CustomSmartPtr() {
if constexpr (is_same_v<ReleasePolicy, CustomDelete>) {
ReleasePolicy::release(ptr); // 自定义删除
} else {
ReleasePolicy::release(ptr); // 普通/数组删除
}
}
};
// 测试:编译期生成不同释放策略的智能指针
class MyClass {};
int main() {
CustomSmartPtr<MyClass> p1(new MyClass()); // 策略:NormalDelete
CustomSmartPtr<int[], ArrayDelete> p2(new int[5]); // 策略:ArrayDelete
CustomSmartPtr<MyClass, CustomDelete> p3(new MyClass()); // 策略:CustomDelete
return 0;
}
核心要点
- TMP 的核心特征:编译期执行,所有计算、逻辑判断在编译阶段完成,生成的可执行文件仅包含最终结果 / 代码,无运行期开销;
- TMP 的实现基础:模板递归、模板特化、编译期常量、
constexpr/if constexpr(C++17+); - TMP 的适用场景:编译期计算、策略化代码生成、类型校验、避免特殊类型的无效代码,适合高性能库、标准库开发。
请记住
Template metaprogramming (TMP,模板元编程) 可将工作从运行期移往编译期,因而得以实现早期错误侦测和更高的运行效率;TMP 可用来生成 "基于政策选择组合"(base on combinations of policy choices) 的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
更多推荐



所有评论(0)