目录
[一、了解隐式接口和编译期多态(Effective C++ 41)](#一、了解隐式接口和编译期多态(Effective C++ 41))
[二、了解 typename 的双重意义(Effective C++ 42)](#二、了解 typename 的双重意义(Effective C++ 42))
[三、学习处理模板化基类内的名称(Effective C++ 43)](#三、学习处理模板化基类内的名称(Effective C++ 43))
[四、将与参数无关的代码抽离 templates(Effective C++ 44)](#四、将与参数无关的代码抽离 templates(Effective C++ 44))
[五、运用成员函数模板接受所有兼容类型(Effective C++ 45)](#五、运用成员函数模板接受所有兼容类型(Effective C++ 45))
[六、需要类型转换时请为模板定义非成员函数(Effective C++ 46)](#六、需要类型转换时请为模板定义非成员函数(Effective C++ 46))
[七、请使用 traits classes 表现类型信息(Effective C++ 47)](#七、请使用 traits classes 表现类型信息(Effective C++ 47))
[八、认识 template 元编程(Effective C++ 48)](#八、认识 template 元编程(Effective C++ 48))
[现代 C++:TMP 已经全自动化了:](#现代 C++:TMP 已经全自动化了:)
一、了解隐式接口和编译期多态(Effective C++ 41)
C++还支持一种不依赖virtual的多态,它在编译期实现,且依靠隐式接口
| 类型 | 绑定时机 | 机制 | 示例 | 关键特征 |
|---|---|---|---|---|
| 运行期多态 | 运行时(dynamic binding) | virtual 函数 + 继承 |
Shape* s = new Circle; s->draw(); |
需要指针/引用,性能略低 |
| 编译期多态 | 编译时(static binding) | 模板 (template) + 隐式接口 | draw(c); 其中 c 有 draw() 函数 |
无 virtual 开销、更灵活 |
在OOP(面向对象编程)继承中,接口是显式声明的:
class Shape {
public:
virtual void draw() const = 0; // 显式接口
};
所有继承 Shape 的类都必须实现 draw()。
而在 C++ 模板世界里------
接口是隐式的:
只要类"有某个成员函数 ",模板就可以用。
编译器在实例化时自动检查。
#include <iostream>
using namespace std;
struct Circle {
void draw() const { cout << "Draw circle\n"; }
};
struct Square {
void draw() const { cout << "Draw square\n"; }
};
template <typename T>
void render(const T& shape) {
shape.draw(); // 编译期多态:只要求 T 有 draw()
}
int main() {
Circle c; Square s;
render(c);
render(s);
}
// 输出
Draw circle
Draw square
没有任何继承,也没有 virtual。但 render() 对不同类型对象表现出不同行为------这就是编译期多态 。
编译器只看:
"你传进来的 T 有没有这个函数名?"
这就是编译期多态。
| 对比维度 | 显式接口(继承) | 隐式接口(模板) |
|---|---|---|
| 定义方式 | 声明 virtual 函数 | 只要存在可用函数 |
| 绑定时机 | 运行时 dynamic binding | 编译时 static binding |
| 类型要求 | 必须继承 Base 并 override | 只需满足函数调用条件 |
| 调用开销 | 有 vtable 查找 | 无 vtable 开销 |
| 灵活性 | 稳定、结构清晰 | 更灵活、类型可混合 |
| 错误检查 | 运行时(常见逻辑错) | 编译期(编译器报错) |
编译期模板的典型场景:
1、模板函数
template <typename T>
void swapValues(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
只要 T 支持赋值运算,编译器就能实例化。
2、模板类(泛型容器)
template <typename T>
class Vector {
public:
void push_back(const T& value);
};
T 只需要能被复制,不要求继承任何基类。
3、策略模式(Policy Based Design)
template <typename SortingPolicy>
class Container : private SortingPolicy {
public:
void sort() { this->sortImpl(); }
};
struct QuickSort { void sortImpl() { cout << "QuickSort\n"; } };
struct MergeSort { void sortImpl() { cout << "MergeSort\n"; } };
Container<QuickSort> a; a.sort(); // QuickSort
Container<MergeSort> b; b.sort(); // MergeSort
这就是典型的"编译期策略多态"。
| 特点 | 运行期多态 (virtual) | 编译期多态 (template) |
|---|---|---|
| 绑定时机 | 运行时 | 编译时 |
| 灵活性 | 支持基类指针统一操作 | 要求类型在编译时已知 |
| 性能 | 有 vtable 开销 | 无运行时开销 |
| 错误发现 | 运行时 / 逻辑错 | 编译时报错 |
| 可扩展性 | 新类型需继承基类 | 新类型只要满足接口形式 |
| 典型用途 | 插件系统、接口多态 | 泛型算法、STL |
virtual 函数 → 显式接口 + 运行期多态
模板 → 隐式接口 + 编译期多态
前者结构化、稳定;
后者灵活、高效
二、了解 typename 的双重意义(Effective C++ 42)
两种意义
| 用法类别 | 意义 | 示例 | 说明 |
|---|---|---|---|
| ① 声明模板参数时 | 表示"类型参数" | template <typename T> |
✅ 等价于 template <class T> |
| ② 在模板定义内部使用时 | 告诉编译器 "这是一个类型名" | typename T::value_type |
❗ 这时必须写 typename,否则编译不过 |
1.模板参数
template <typename T>
void func(T param);
//等于
template <class T>
void func(T param);
这里 typename 只是告诉编译器:
"T 是一个类型参数"。
在模板参数列表里,typename 和 class 没有区别。
2.在模板内部的"嵌套依赖类型"
//错误写法
template <typename T>
void printFirst(const T& container) {
T::const_iterator iter = container.begin(); // ❌ 编译错误
std::cout << *iter << std::endl;
}
//报错:
error: need 'typename' before 'T::const_iterator' because 'T' is a dependent scope
在模板定义阶段,编译器看到T::const_iterator,它不知道
"T::const_iterator 是一个类型?还是一个静态成员变量?还是别的符号?"
此时 T 是一个 dependent name (依赖于模板参数),
C++ 语法要求你必须显式告诉编译器:
"放心,这是一个类型名。"
//正确写法:
template <typename T>
void printFirst(const T& container) {
typename T::const_iterator iter = container.begin(); // ✅ OK
std::cout << *iter << std::endl;
}
什么时候必须写typename
当在模板里使用依赖于模板参数的嵌套类型时,必须加typename
即:
-
T::iterator -
U::value_type -
MyType::NestedType
| 语句 | 是否需要 typename |
解释 |
|---|---|---|
T::iterator it; |
✅ 需要 | T 未知,依赖模板参数 |
typename T::iterator it; |
✅ 正确 | |
MyClass::iterator it; |
❌ 不需要 | 已知类型,不依赖模板参数 |
template <typename T> class A {}; |
✅ 必须 | 声明模板参数 |
template <class T> class A {}; |
✅ 同等合法 |
什么时候不能加typename
1、在模板参数声明里
template <typename T> // ✅ OK
class Container {};
2、在非类型依赖的名字前
class Foo {
public:
using value_type = int;
};
template <typename T>
void f() {
Foo::value_type x; // ❌ 不能写 typename Foo::value_type
}
总结
1.在模板参数声明中:
→ 表示「这是一个类型参数」
(等价于 class)
2.在模板定义内部:
→ 表示「这是一个依赖于模板参数的嵌套类型」
(告诉编译器:T::xxx 是类型,不是变量)
三、学习处理模板化基类内的名称(Effective C++ 43)
当你的派生类是模板类,而它继承子一个模板基类时,派生类内部访问基类的成员时,编译器可能找不到那些名字。
#include <iostream>
using namespace std;
template <typename T>
class Base {
protected:
void printBase() { cout << "Base::printBase()" << endl; }
int baseValue = 10;
};
template <typename T>
class Derived : public Base<T> {
public:
void func() {
printBase(); // ❌ 错误:编译器说找不到 printBase()
cout << baseValue << endl; // ❌ 同样报错
}
};
编译错误:
error: 'printBase' was not declared in this scope
error: 'baseValue' was not declared in this scope
derived继承了base但是访问不到base的函数,原因是因为c++编译器在模板定义阶段不会"假设"
任何模板参数有关的名字。
编译器思考过程:
1.当编译器定义Derived<T>时,看到
class Derived : public Base<T> {};
此时它知道:
-
Base 是一个模板;
-
T 是个未知类型;
-
Base<T> 的内容取决于 T。
2.当它看到 printBase();
他会问:这个名字的作用域在哪里
编译器查找顺序:
1.当前类(Derived<T>)的作用域;
2.外层作用域(命名空间、全局)
3.不会自动去查基类模板的作用域!(因为基类依赖模板参数)
我现在还不知道 Base<T> 里到底有没有 printBase(),所以我暂时不敢去那查。
结果:编译器不敢假设基类模板的内容,于是报告"找不到符号"
解决方案:
1.加上this->
告诉编译器,"这个成员来自当前对象(可能在基类里)"
template <typename T>
class Derived : public Base<T> {
public:
void func() {
this->printBase(); // ✅ OK
cout << this->baseValue; // ✅ OK
}
};
啊,你写了this->printBase,那就是说这个名字是"当前对象"的成员咯?
当前对象是谁嘞?是 Devived<T>的实例,而Derived<T>继承了Base<T>。
所以我知道了,
我要去当前对象(this)的所有成员里找
包括它的基类成员
虽然Base<T>的定义要等实例化时才确定 ,但我知道"到时候可以去查"。
好的!那我可以把它延迟到实例化时解析!
2.用作用域限定Base<T>::
template <typename T>
class Derived : public Base<T> {
public:
void func() {
Base<T>::printBase(); // ✅ OK
cout << Base<T>::baseValue; // ✅ OK
}
};
直接告诉编译器"去 Base<T> 找这个名字"。
缺点是:如果 Base<T> 名字很长,这种写法比较繁琐。
3.用 using 声明引入名字
template <typename T>
class Derived : public Base<T> {
public:
using Base<T>::printBase;
using Base<T>::baseValue;
void func() {
printBase(); // ✅ OK
cout << baseValue << endl; // ✅ OK
}
};
把基类的名字显式"导入"到当前作用域中。
这是最干净的做法,也最推荐在大型项目中使用。
写法对比:
| 写法 | 可读性 | 适用场景 | 原理 |
|---|---|---|---|
this->func() |
简短、常用 | 成员函数调用 | 显式告诉编译器"这是对象成员" |
Base<T>::func() |
明确、繁琐 | 静态成员或不在 this 上下文 | 显式指定查找路径 |
using Base<T>::func; |
最清晰 | 多个成员导入 | 把名字引入当前作用域 |
为什么非模板类不需要那么麻烦?
这时候编译器知道Base的结构是固定的,不依赖模板参数,所以它能直接在基类作用域查找。
模板类不同,因为Base<T>在每个T下都不同。编译器在定义阶段下不能确定基类是否该有名字,必须要手动"引导查找"
四、将与参数无关的代码抽离 templates(Effective C++ 44)
模板会被编译器复制多份,如果某段代码与模板参数无关,就会被重复复制多次--浪费时间又浪费时间,所以要把它抽出去(放到非模板的地方)。
简单来说就是:
模板里值放"跟类型T真正相关的东西"。不相关的东西别放进去,会被复制好几份。
例子1:
template<typename T>
void foo(const T& x) {
std::cout << "Hello\n"; // ❌ 和 T 完全无关
std::cout << x << "\n"; // ✅ 和 T 有关
}
//如果调用
foo(10); // T = int → 生成一份代码
foo(3.14); // T = double → 再生成一份代码
foo("test"); // T = const char* → 再生成一份
那一行 "Hello" 被重复生成三次。
如果你 foo<T> 实例化 50 次,那段代码就重复 50 次
(占50倍空间、编译 50 次)。
如果代码是几百行,那就炸了。
解决:
cpp
void printHello() {
std::cout << "Hello\n"; // 只生成一次
}
template<typename T>
void foo(const T& x) {
printHello(); // 调用即可,不重复生成
std::cout << x << "\n";
}
现在 printHello():
-
不管 foo<int>
-
foo<double>
-
foo<string>
它都只存在一份。
例子2:类模板
模板类的每个成员函数都属于类模板本身。
类模板实例化=生成一个完整类,包括它的所有成员函数。
错误:
cpp
template<typename T>
class Parser {
public:
void parse(const T& t) {
checkHeader(); // ❌ 这个函数根本与 T 无关,每次都会复制多份
parseImpl(t); // 只有这里才和 T 有关
}
private:
void checkHeader() { // ❌ 会被重复 N 份
std::cout << "Checking\n";
}
void parseImpl(const T& t) { ... } // 和 T 有关
};
正确:
cpp
class ParserBase {
protected:
void checkHeader() {
std::cout << "Checking\n"; // 只存在 1 份!
}
};
template<typename T>
class Parser : private ParserBase {
private:
void parseImpl(const T&);
public:
void parse(const T& t) {
checkHeader(); // 不重复
parseImpl(t); // 和T有关
}
};
五、运用成员函数模板接受所有兼容类型(Effective C++ 45)
如果希望一个类能接受"所有可以转换成它的类型",那么你应该写"模板化的成员函数",而不是普通函数。
尤其是:模板复制构造函数,模板赋值运算符。
比如:
cpp
class SmartPtr {
public:
SmartPtr(SomeClass* ptr) : p(ptr) {}
private:
SomeClass* p;
};
个类只能接受:
SmartPtr sp(new SomeClass);
但如果你有一个派生类:
class Derived : public SomeClass {};
你想这样写:
SmartPtr sp(new Derived); // ❌ 不能自动转换
C++ 不会把 Derived* 自动转为 SomeClass*
(因为构造函数不是模板)。
使用成员函数模板就能解决:
cpp
class SmartPtr {
public:
template<typename T>
SmartPtr(T* ptr) : p(ptr) {} // ❤️ 接受所有可以转为 SomeClass* 的类型
private:
SomeClass* p;
};
cpp
SmartPtr sp1(new SomeClass); // OK
SmartPtr sp2(new Derived); // OK!转换到 SomeClass*
这就是"成员函数模板接受兼容类型"。
cpp
std::shared_ptr<Base> pb = std::make_shared<Derived>();
之所以可以接受derived,是因为它内部写了
cpp
template <class Y>
shared_ptr(const shared_ptr<Y>& r);
而不是:
cpp
shared_ptr(const shared_ptr& r); // ❌ 只能接受同类型
注意:
虚函数不能是模板(c++不允许),所以这条主要用于构造/赋值。
本条的真正价值:
1.写更通用的类(智能指针,容器)、
2.无需繁琐写多个构造函数
cpp
BasePtr(Derived* p) {}
BasePtr(Derived2* p) {}
BasePtr(Derived3* p) {}
//变成
template<typename T>
BasePtr(T* p) {}
3.避免不必要的强转
完整示例:
cpp
template<typename T>
class SmartPtr {
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other) : ptr(other.ptr) {}
T* ptr;
};
cpp
SmartPtr<Base> pb;
SmartPtr<Derived> pd;
pb = pd; // ❤️ 自动兼容
这里有两个模板,但不是"套模板",而是"模板里的成员函数本身也是模板"。
总结:
如果你的类希望能接受"任何可以转为它的类型",
最好的方法就是:写"成员函数模板"。
比如:
-
模板化构造函数
-
模板化复制构造
-
模板化赋值运算符
-
模板化转换
这是写通用库(包括 STL、智能指针、policy-based design)的基础技巧。
六、需要类型转换时请为模板定义非成员函数(Effective C++ 46)
如果你写一个模板类,并且这个类需要支持隐式类型转换(例如用于运算符重载),那么相关的运算符函数应该写成"非成员函数(friend或外部函数)",而不是成员函数。(看不懂啊看不懂)
原因:
成员函数不支持两边的隐式转换,而非成员函数可以。
成员函数 operator*
只能让 右边 的操作数发生隐式类型转换。
但不能让左边发生转换 ,因为左边被绑定成了 this,必须已经是类类型。
cpp
template<typename T>
class Rational {
public:
Rational(const T& num, const T& den) : n(num), d(den) {}
// 成员版本的 operator*
Rational operator*(const Rational& rhs) const {
return Rational(n * rhs.n, d * rhs.d);
}
private:
T n, d;
};
普及常识:
| 函数名 | 重载的是 |
|---|---|
operator+ |
加法 |
operator- |
减法 |
operator* |
乘法 |
operator/ |
除法 |
operator[] |
下标运算符 |
operator() |
函数调用运算符 |
operator-> |
成员访问运算符 |
operator== |
相等比较 |
现在做一个非常正常的操作:
cpp
Rational<int> r(1, 2);
auto x = r * 2; // ❌ 不工作
因为 operator* 是"成员函数":
cpp
r.operator*(2);
而它的参数要求是 Rational<int>,
2 并不会自动变成 Rational<int>。
所以成员函数不支持:
右侧参数的隐式转换
你只允许:
cpp
r * Rational<int>(2);
这样就太难用了
换成非成员函数就可以:
cpp
template<typename T>
class Rational {
// ...
// 让它成为友元,便于访问 n 和 d
template<typename U>
friend Rational<U> operator*(const Rational<U>& lhs, const Rational<U>& rhs);
};
template<typename U>
Rational<U> operator*(const Rational<U>& lhs, const Rational<U>& rhs) {
return Rational<U>(lhs.n * rhs.n, lhs.d * rhs.d);
}
就可以:
cpp
Rational<int> r(1, 2);
auto a = r * 2; // ❤️ 自动把 2 转换成 Rational<int>
auto b = 2 * r; // ❤️ 左边也可以自动转换!
这就是 Scott Meyers 强调的:"当你需要类型转换能力,以非成员函数实现。"'
完整正确写法:
cpp
template<typename T>
class Rational {
public:
Rational(const T& n = 0, const T& d = 1) : num(n), den(d) {}
template<typename U>
friend Rational<U> operator*(const Rational<U>& lhs,
const Rational<U>& rhs);
private:
T num, den;
};
template<typename U>
Rational<U> operator*(const Rational<U>& lhs, const Rational<U>& rhs) {
return Rational<U>(lhs.num * rhs.num, lhs.den * rhs.den);
}
现在所有组合都支持:
cpp
Rational<int> r(1, 3);
r * 5; // OK
5 * r; // OK
r * r; // OK
总结:
当模板类需要隐式类型转换(尤其是运算符重载)时,
运算符应该写成"非成员函数模板",而不是成员函数。
七、请使用 traits classes 表现类型信息(Effective C++ 47)
这条是模板里最重要的一条。
traits class 是一个模板结构,用来"在编译期告诉你某个类型的特性"
换句话说:
我怎么让模板知道T是不是指针?是不是迭代器?他的value_type是啥?T能不能加法?T是所有符号的?
这些信息都不能在T本身写代码解决,但可以用traits!
cpp
template<typename T>
void process(const T& x) {
if ( ??? ) { // 如果 T 是指针怎么办?
// 做指针特化逻辑
} else {
// 做一般类型逻辑
}
}
问题来了:
模板里你没办法写"if (T is pointer)"
因为 T 只是一个类型,不是值!
你无法直接比较类型,
但你需要一种机制:
让模板根据 T 的"类型特征"选择不同逻辑。
这就是 traits class 的使命。
traits的基本格式:
cpp
template<typename T>
struct TypeTraits {
static const bool isPointer = false;
};
// 针对指针模板特化
template<typename U>
struct TypeTraits<U*> {
static const bool isPointer = true;
};
第一个模板:泛化版本
给所有类型的默认值,任何类型如果没有特殊处理,都认为不是指针
举例:
-
int→ isPointer = false -
double→ isPointer = false -
std::string→ isPointer = false
第二个模板:偏特化版本
专门处理指针类型,当T是指针,这个特化就匹配。
-
int*→ 匹配TypeTraits<U*>→ isPointer = true -
double*→ 匹配TypeTraits<U*>→ isPointer = true -
MyClass*→ 匹配TypeTraits<U*>→ isPointer = true
现在我们可以写:
cpp
template<typename T>
void process(const T& x) {
if (TypeTraits<T>::isPointer) {
std::cout << "T 是指针类型\n";
} else {
std::cout << "T 是非指针类型\n";
}
}
使用
cpp
int x = 10;
int* p = &x;
process(x); // → T 是非指针类型
process(p); // → T 是指针类型
STL里最典型的traits:iterator_traits(迭代器特性)
cpp
template<typename Iter>
void advance(Iter& it, int n) {
if (??? it 是否是 random_access_iterator ???)
it += n; // O(1)
else
while (n--) ++it; // O(n)
}
你怎么知道it哪种迭代器?
STL 用 traits:
cpp
template<typename Iter>
struct iterator_traits {
typedef typename Iter::iterator_category iterator_category;
};
然后可以这么写:
cpp
template<typename Iter>
void advance(Iter& it, int n) {
typedef typename iterator_traits<Iter>::iterator_category Cat;
advanceImpl(it, n, Cat()); // 根据类型不同选不同函数
}
typedef: 给类型名起名
typename:告诉编译器,这是类型,不是成员变量。
typename Iter::iterator_category:找到Iter这个类型内部名为iterator_category的类型
比如 vector 的迭代器有:
cpp
struct __vector_iterator {
using iterator_category = random_access_iterator_tag;
};
typedef ... iterator_category:给这个类型重新起一个短名字叫 iterator_category。
然后就可以这么写:
cpp
template<typename Iter>
void advance(Iter& it, int n) {
typedef typename iterator_traits<Iter>::iterator_category Cat;
advanceImpl(it, n, Cat()); // 根据类型不同选不同函数
}
最终实现智能调度:
cpp
template<typename Iter>
void advanceImpl(Iter& it, int n, random_access_iterator_tag) {
it += n; // 快
}
template<typename Iter>
void advanceImpl(Iter& it, int n, input_iterator_tag) {
while (n--) ++it; // 慢
}
完整例子:
cpp
#include <iostream>
using namespace std;
struct MyIter {
using iterator_category = int; // 这是一个类型(别管具体是什么)
};
template<typename Iter>
struct iterator_traits {
typedef typename Iter::iterator_category iterator_category;
};
int main() {
// 等价于 int x; 因为 MyIter::iterator_category = int
iterator_traits<MyIter>::iterator_category x = 123;
cout << x << endl;
}
总结:
模板本身不知道类型信息,但 traits 能告诉模板"这个类型的特性"。
traits 是泛型编程的基础,可以让你做类型判断、类型选择、类型属性提取。
八、认识 template 元编程(Effective C++ 48)
模板元编程(Template Metaprogramming, TMP)是:让C++的模板系统在"编译期"执行"计算或逻辑",然后把结果用于生成最终的代码。
换句话说:
平时我们的代码是运行时执行
TMP的代码时编译器在编译时执行
你写的模板代码,在编译期间先跑一遍,产出结果,再用这些结果生成最终的可执行程序。
所以:模板元编程≈编译器程序语言
为什么要有这个东西?
因为它可以做三件普通 C++ 做不到的事情:
- 编译期计算 → 性能极高
例如常数折叠、编译期生成查表、提前优化。
- 编译期分派(dispatch)→ 更快,而不是 runtime(运行时) 判断
tips:dispatch="决定调用哪个函数" 的过程。
例如:
cpp
if (iterator 是 random access) 用 +=
else 用 ++
这些逻辑如果运行时判断会慢,
TMP 让你在编译期"算出应该走哪个逻辑"。
- 自动生成大量重复代码(泛型)
比如 STL 之类的库,靠 TMP 自动生成无数模板类型。
最简单的例子:
cpp
template<int n>
struct Factorial {
static const int value = n * Factorial<n-1>::value;
};
template<>
struct Factorial<1> {
static const int value = 1;
};
int x = Factorial<5>::value; // 5! = 120,编译期就算好了
你没运行程序,它就算出来了。
这是最经典的 TMP pattern。
实际例子(其实和之前的例子是一样的):
cpp
template<typename T>
struct IsPointer {
static const bool value = false;
};
template<typename U>
struct IsPointer<U*> {
static const bool value = true;
};
cpp
std::cout << IsPointer<int>::value << std::endl; // 0
std::cout << IsPointer<int*>::value << std::endl; // 1
这个判断是编译期完成的!
实际用途:
| 应用 | 说明 |
|---|---|
| 类型判断 | is_pointer, is_const, is_same |
| 条件编译 | enable_if、conditional |
| 选择不同算法 | random access vs input iterator |
| 自动推断类型 | iterator_traits |
| 生成更高性能代码 | loop unrolling、constexpr |
| 编写泛型库 | STL、Boost、Eigen 全在用 |
TMP的核心机制
1.模板的递归实例化能力
模板可以根据类型或整数无限递归生成新模型:
cpp
template<int N> class A {
A<N-1> next; // 递归生成类型链
};
2.编译器常量
TMP 必须依赖编译期常数
cpp
static const int value = n * Factorial<n-1>::value;
比如:
cpp
template<int N>
struct A {};
// 不能写
int x = 10;
A<x> a; // ❌ 不行
因为 x 只有运行期才有值。
因为 x 是一个 运行期变量:
-
它存储在栈里
-
程序运行后才初始化
-
编译器只知道"有个变量 x",不知道它的值是多少
3.模板特化(specialization)
TMP 的"if / else"就是靠特化实现:
cpp
template<int N> struct Factorial { ... }; // 一般情况
template<> struct Factorial<1> { ... }; // 特殊情况
现代 C++:TMP 已经全自动化了:
在 C++11/14/17 之后,几乎不需要写递归 TMP。
因为标准库已经提供大量 TMP 工具:
-
std::conditional -
std::enable_if -
std::integral_constant -
std::true_type / false_type -
std::is_pointer -
std::is_same -
std::remove_reference -
std::declval -
std::is_convertible -
std::is_trivial
只需要使用即可。
现代 TMP 还加入了:
-
constexpr if -
constexpr function -
concepts -
requires
已经让 TMP 简洁很多。
总结:
模板元编程 = 利用模板系统在编译时做计算与逻辑,从而选择不同代码或优化执行效率。
TMP 的基础是:模板递归 + 特化 + 编译期常量。
只要理解:
-
traits 是 TMP 的应用
-
iterator dispatch 是 TMP
-
is_pointer / is_same 是 TMP
-
SFINAE、enable_if 是 TMP
就已经掌握了 99% 的 TMP。