ARTICLE · 1213935
C++模版编程演进
C++98/03:混沌初开
模板设计的初衷:实现 泛型容器 (比如std::vector<T> )。之后意外发现:利用模板特化和递归,居然可以在编译期做计算!
核心机制
类模板与函数模板 :基础的泛型能力。 模板特化 / 偏特化 :相当于编译期的 if-else 。 SFINAE(Substitution Failure Is Not An Error) :“替换失败不是错误”。早期做编译期类型检查的唯一手段,极其晦涩。
解决了什么问题
代码复用(如 STL 容器)。 初步的编译期计算(如编译期求阶乘)。
痛点(恶梦)
没有可变参数,想传任意数量的参数得写几十个重载(如早期的 boost::bind )。 SFINAE 写法极其反人类(各种 enable_if 、 sizeof 魔法)。 报错信息长达几百行,根本看不懂。
C++11:现代模板的奠基(革命性升级)
一次彻底的革命,引入了大量特性,让模板编程真正具备了工业可用性。
引入的关键特性
可变参数模板(Variadic Templates)
作用:可以接收任意数量、任意类型的模板参数( template<typename… Args> )。可以优雅地写出 std::tuple 和完美的 printf 替代品了,不再需要手写 1 到 10 个参数的重载。
尾置返回类型(Trailing Return Type)
作用 :让编译器帮忙推导表达式的类型。
1 2
// 在泛型编程中,如果不知道 t1 + t2 会返回什么类型,用 decltype 能完美解决。
auto add(T1 t1, T2 t2) -> decltype(t1 + t2) 别名模板(Alias Templates / using )
作用 :替代晦涩的 typedef 。比如 :
1 2
template<typename T>
using MyVec = std::vector<T, MyAlloc<T>>; 这是在 typedef 时代是做不到的( typedef 不支持模板)。
std::enable_if 标准化
作用 :把 SFINAE 这种黑魔法正式纳入标准库,提供了一种相对标准的条件编译方式。
外部模板(Extern Templates)
作用 :减少模板实例化带来的编译时间爆炸。
1 2
// 在头文件里声明:告诉编译器“别在这里实例化,别处有现成的”
extern template class std::vector<HeavyType>; 用一次集中的、显式的实例化定义,换取所有其他编译单元里隐式实例化工作的豁免。不改变程序的运行时行为,纯粹是编译期和链接期的空间与时间优化。
C++14:更好用的小修小补
解决很多 C++11 遗留的“别扭”问题。
引入的关键特性
变量模板(Variable Templates)
解决的问题 :允许模板定义变量,而不仅是类和函数。比如
1 2 3 4
template <typename T> constexpr DrawableVTable vtable_for = {...}
// 定义编译期常量
template<class T> constexpr T pi = T(3.1415926535897932385); 普通函数的返回类型推导(Return Type Deduction)
解决的问题 :不再强制要求写尾置 decltype 。直接写
1
auto add(T1 t1, T2 t2){ return t1 + t2; } 泛型 Lambda(Generic Lambdas)
1
[] (auto x, auto y) { return x + y; } C++17:从“元编程”走向“编译期编程”
核心目标是: 干掉晦涩的模板递归和 SFINAE,让编译期代码看起来像普通的运行时代码。
引入的关键特性
constexpr if (编译期 if) : 终结了大量 SFINAE 和模板特化! 以前为了在编译期做条件分支,必须写多个模板重载;现在直接在一个函数里写:
1 2
// 编译器会自动把不需要的分支丢弃掉
if constexpr (std::is_integral_v<T>){ ... } else { ... } 折叠表达式(Fold Expressions) : 终结了处理可变参数模板时的递归! 以前展开 Args… 必须写递归函数和终止条件;现在直接写 (args + …) 就能把所有参数加起来。
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
#include <type_traits>
// 旧版 C++11/14
template<typename... Args>
auto sum(Args... args){
using T = std::common_type_t<Args..., int>; // 加 int 保证空包也有类型
T result{};
int dummy[] = { 0, (result += args, 0)... }; // 加 0 保证数组非空
(void)dummy;
return result;
}
// 新版 C++17s
template<typename... Args>
auto sum(Args... args){
return (0 + ... + args); // 等价
}
/* 打印参数 */
// 旧版 C++11/14
void print(){ std::cout << '\n'; } // 终止条件
template<typename T, typename... Args>
void print(T first, Args... rest){
std::cout << first << ' ';
print(rest...);
}
// 新版 C++17
template<typename... Args>
void print(Args... args){
((std::cout << args << ' '), ...); // 逗号折叠
std::cout << '\n';
}
/* 检查是否所有参数都满足某个条件 */
// 旧版 C++11/14
bool all_true(){ return true; }
template<typename... Args>
bool all_true(bool first, Args... rest){
return first && all_true(rest...);
}
// 新版 C++17
template<typename... Args>
bool all_true(Args... args){
return (args && ...); // 所有为 true 才返回 true
}
/* 在容器里依次插入元素 */
// 新版 C++17
template<typename Container, typename... Args>
void push_all(Container& c, Args... args){
(c.push_back(args), ...); // 依次 push_back
} 特别注意:折叠表达式有一个二元左右折叠的注意细节,这里不展开介绍。
类模板参数推导(CTAD) :实例化模板类时不需要再手写尖括号了。以前必须写 std::pair<int, double> p(1, 2.0); ,现在直接写 std::pair p(1, 2.0); ,编译器自动推导。这也是为什么 std::make_pair 等辅助函数逐渐退出历史舞台的原因。
内联变量( inline variables) :不是专门针对模板,解决了 header-only 模板库中全局变量重复定义的痛点。
C++20:概念(Concepts),模板的最终形态
引入了 C++ 历史上酝酿最久(超过 15 年)的特性:Concepts。它彻底改变了模板编程的思维方式。
引入的关键特性:
Concepts 与 Requires 表达式: 彻底终结 SFINAE! 以前模板就像是一个没有类型检查的黑盒,只有在实例化报错时你才知道传错了类型,而且报错信息是极其恐怖的“瀑布式报错”。
1 2 3 4 5 6 7 8 9
// 有了 Concepts,你可以像定义接口一样约束模板参数。
//定义约束:
template<typename T>
concept Addable = requires(T a, T b) { a + b; };
//使用约束
template<Addable T>
T add(T a, T b){ return a + b; } 模板支持 Lambda :现在可以把 Lambda 表达式直接塞进模板参数里(比如作为 std::set 或智能指针的自定义删除器),无需再单独定义一个仿函数类。
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
#include <iostream>
#include <set>
#include <memory>
#include <vector>
#include <algorithm>
int main(){
// ============================================================
// 场景 1:Lambda 作为 std::set 的比较器
// ============================================================
// 以前:必须单独定义一个 struct,重载 operator()
// struct Greater { bool operator()(int a, int b) const { return a > b; } };
// std::set<int, Greater> s;
//
// 现在:直接传 lambda 的类型作为模板参数
auto cmp = [](int a, int b) { return a > b; }; // 降序比较
std::set<int, decltype(cmp)> s(cmp);
s.insert({3, 1, 4, 1, 5, 9, 2, 6});
std::cout << "set (descending): ";
for (int x : s) std::cout << x << ' ';
std::cout << '\n';
// ============================================================
// 场景 2:Lambda 作为 unique_ptr 的自定义删除器
// ============================================================
// 管理 malloc 分配的内存,用 free 释放
auto free_deleter = [](int* p) {
std::cout << "freeing " << *p << '\n';
std::free(p);
};
// decltype(free_deleter) 作为 unique_ptr 的第二个模板参数
// Lambda 的类型是匿名的,必须用 `decltype` 取
std::unique_ptr<int, decltype(free_deleter)> p1(
static_cast<int*>(std::malloc(sizeof(int))),
free_deleter
);
*p1 = 42;
// ============================================================
// 场景 3:Lambda 作为 shared_ptr 的删除器(更简洁)
// ============================================================
// shared_ptr 的删除器类型不需要出现在模板参数里,直接用 lambda
std::shared_ptr<FILE> fp(
std::fopen("/tmp/test_lambda.txt", "w"),
[](FILE* f) {
if (f) {
std::cout << "closing file\n";
std::fclose(f);
}
}
);
if (fp) {
std::fputs("hello from lambda deleter\n", fp.get());
}
// ============================================================
// 场景 4:Lambda 作为 sort 的自定义比较(非模板参数,但常见)
// ============================================================
std::vector<int> v = {5, 2, 8, 1, 9};
std::sort(v.begin(), v.end(), [](int a, int b) {
return a % 2 < b % 2; // 偶数在前,奇数在后
});
std::cout << "sorted by parity: ";
for (int x : v) std::cout << x << ' ';
std::cout << '\n';
return 0;
}更强大的 constexpr :现在你甚至可以在编译期动态分配内存、使用 STL 容器做复杂的计算,计算完后把结果固化在只读数据段里。
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
#include <vector>
#include <array>
#include <iostream>
// C++20:constexpr 函数里可以用 std::vector
constexpr auto build_primes(int n){
std::vector<int> primes;
for (int i = 2; i <= n; ++i) {
bool is_prime = true;
for (int p : primes) {
if (p * p > i) break;
if (i % p == 0) { is_prime = false; break; }
}
if (is_prime) primes.push_back(i);
}
return primes; // 返回 vector,编译期计算
}
int main(){
// 编译期计算,结果固化
constexpr auto primes = build_primes(50);
static_assert(primes.size() == 15); // 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47
static_assert(primes[0] == 2);
static_assert(primes[14] == 47);
for (int p : primes) std::cout << p << ' ';
} 更贴近实际使用经验的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// C++ 17
constexpr auto build_primes_17(){
std::array<int, 15> primes{}; // 预先知道结果有多少个元素。 15个
int count = 0;
// ... 手动管理 count,不能 push_back
return primes;
}
// C++ 20
constexpr auto build_primes_20(int n){
std::vector<int> primes; //不需要预知
// ... primes.push_back(i)
return primes;
} 一些限制:
编译期分配的内存不能泄露 不是所有 std::vector操作都能在编译期用编译期求值有步数限制 编译时间会变长
总结:演进的主线
回顾这段历史,C++ 模板编程的演进主线非常清晰:
C++98 / C++11 :解决了“能不能做”的问题。引入可变参数、 decltype 。
C++11 / C++17 :解决了“好不好写”的问题。引入 if constexpr 、折叠表达式,干掉特化。
C++17 / C++20 :解决了“好不好读、安不安全”的问题。引入 Concepts,规范模板契约,消灭报错瀑布。















































































