C++ 进阶编程:模板与元编程笔记

C++ 进阶编程:模板与元编程笔记

时间:2025/12/16

关键词:泛型编程、编译期计算、零成本抽象、类型推导、约束、类型萃取
核心目标:把“同一套逻辑支持多种类型”这件事交给编译器完成。


1. 模板到底在解决什么问题

如果没有模板,很多函数只能为不同类型重复写多份:

1
2
3
int twice(int x) { return x * 2; }
float twice(float x) { return x * 2; }
double twice(double x) { return x * 2; }

模板的作用就是把“变化的类型”抽象成参数:

1
2
3
4
template <class T>
T twice(T x) {
return x * 2;
}

这样带来的好处:

  • 一份代码可适配多种类型
  • 类型检查发生在编译期
  • 通常没有运行时额外开销
  • 很适合高性能场景中的“零成本抽象”

2. 函数模板

2.1 最基本写法

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

template <class T>
T twice(T x) {
return x * 2;
}

int main() {
std::cout << twice(2) << '\n'; // T 推导为 int
std::cout << twice(3.5) << '\n'; // T 推导为 double
}

classtypename 在模板类型参数里通常等价:

1
2
template <typename T>
T twice(T x) { return x * 2; }

2.2 显式指定模板参数

有时可以手动指定:

1
std::cout << twice<int>(2) << '\n';

但绝大多数情况下,函数模板都可以依靠参数自动推导。

2.3 模板不是“自动重载一切”

模板函数会参与重载决议,但它不是“万能匹配”。
如果你还写了一个普通函数,编译器会按重载规则选择更合适的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <string>

template <class T>
T twice(T x) {
return x * 2;
}

std::string twice(const std::string& s) {
return s + s;
}

int main() {
std::cout << twice(10) << '\n';
std::cout << twice(std::string("hi")) << '\n';
}

这里字符串版本不是“模板自动生成”的,而是我们手动提供了一个更合适的重载。


3. 类模板

函数可以模板化,类也可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
template <class T>
struct Box {
T value;

T get() const {
return value;
}
};

int main() {
Box<int> a{42};
Box<double> b{3.14};
}

类模板常见用途:

  • 容器:std::vector<T>
  • 智能指针:std::unique_ptr<T>
  • 泛型工具类:std::optional<T>std::function<T>

4. 非类型模板参数

模板参数不一定是类型,也可以是编译期常量。

4.1 经典写法

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <string>

template <int N>
void show_times(const std::string& msg) {
for (int i = 0; i < N; ++i) {
std::cout << msg << '\n';
}
}

int main() {
show_times<3>("hello");
}

这里的 N 在编译期就已经确定。

4.2 用编译期布尔值控制分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

template <bool Debug>
int sum_to(int n) {
int res = 0;
for (int i = 1; i <= n; ++i) {
res += i;
if constexpr (Debug) {
std::cout << "i=" << i << ", res=" << res << '\n';
}
}
return res;
}

int main() {
std::cout << sum_to<false>(10) << '\n';
}

if constexpr 的含义是:

  • 条件在编译期判断
  • 不满足条件的分支会被丢弃
  • 很适合模板里的静态分发

4.3 现代 C++ 的扩展

C++17 起可以写:

1
2
template <auto N>
struct ConstValue {};

也就是让非类型模板参数的类型由编译器推导。


5. 模板参数推导与 auto

模板学习里最容易混乱的其实是“类型怎么被推导出来”。

5.1 值传递

1
2
template <class T>
void f(T x) {}

如果传入:

  • int,则 T = int
  • const int,顶层 const 会被忽略,通常还是 T = int
  • 引用也会退化为值

5.2 左值引用

1
2
template <class T>
void f(T& x) {}

此时:

  • 传左值可以
  • const 属性会被保留

例如:

1
2
const int a = 1;
f(a); // T = const int

5.3 万能引用 / 转发引用

1
2
template <class T>
void f(T&& x) {}

T 需要推导且参数形式为 T&& 时,它是转发引用

  • 传右值时,T 推导为普通类型
  • 传左值时,T 推导为左值引用

这是完美转发的基础。

5.4 auto 的推导规则和模板很像

1
2
3
auto x = 1;        // int
const auto y = x; // const int
auto& z = y; // const int&

可以粗略理解为:

  • auto 很像“让编译器帮你写模板参数推导”
  • decltype(expr) 则是“精确获取表达式类型”

6. 函数对象与 lambda

模板经常和“可调用对象”配合使用。

6.1 函数对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

struct Printer {
void operator()(int x) const {
std::cout << x << '\n';
}
};

template <class Func>
void call_twice(Func func) {
func(0);
func(1);
}

int main() {
call_twice(Printer{});
}

Func 可以是:

  • 普通函数指针
  • 仿函数对象
  • lambda

6.2 lambda 本质上也是对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

template <class Func>
void call_twice(const Func& func) {
std::cout << func(1) << '\n';
std::cout << func(2) << '\n';
}

auto make_times(int factor) {
return [=](int n) {
return n * factor;
};
}

int main() {
auto twice = make_times(2);
call_twice(twice);
}

6.3 捕获方式

1
2
3
4
[&]  // 按引用捕获
[=] // 按值捕获
[x] // 只捕获 x
[&x] // 按引用捕获 x

注意:

  • 按引用捕获要注意生命周期
  • 闭包对象会保存按值捕获的副本

6.4 泛型 lambda

C++14 起,lambda 参数可以写 auto

1
2
3
auto print = [](const auto& x) {
std::cout << x << '\n';
};

这相当于编译器帮我们生成了一个带模板 operator() 的闭包类型。


7. if constexpr:模板时代码分支的关键工具

模板中经常需要根据类型走不同逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <type_traits>

template <class T>
void print_info(const T& x) {
if constexpr (std::is_integral_v<T>) {
std::cout << "integral: " << x << '\n';
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "floating: " << x << '\n';
} else {
std::cout << "other type\n";
}
}

相比普通 ifif constexpr 在模板里更重要,因为它可以彻底丢弃无效分支,避免编译错误。


8. 可变参数模板

可变参数模板可以接收任意个模板参数或函数参数。

8.1 基本写法

1
2
template <class... Ts>
void func(Ts... args) {}

这里:

  • Ts... 是模板参数包
  • args... 是函数参数包

8.2 递归展开

1
2
3
4
5
6
7
8
9
#include <iostream>

void print_all() {}

template <class T, class... Ts>
void print_all(const T& first, const Ts&... rest) {
std::cout << first << '\n';
print_all(rest...);
}

8.3 折叠表达式

C++17 起,更推荐用 fold expression:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

template <class... Ts>
void print_all(const Ts&... xs) {
((std::cout << xs << '\n'), ...);
}

template <class... Ts>
auto sum(const Ts&... xs) {
return (xs + ...);
}

这比手写递归更直观。


9. 模板特化

当某些类型需要特殊处理时,可以使用特化。

9.1 全特化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

template <class T>
struct TypeName {
static constexpr const char* get() { return "unknown"; }
};

template <>
struct TypeName<int> {
static constexpr const char* get() { return "int"; }
};

int main() {
std::cout << TypeName<int>::get() << '\n';
}

9.2 偏特化

类模板支持偏特化,函数模板不支持偏特化。

1
2
3
4
5
6
7
8
9
template <class T>
struct IsPointer {
static constexpr bool value = false;
};

template <class T>
struct IsPointer<T*> {
static constexpr bool value = true;
};

这里 T* 就是“对一类类型进行特化”。


10. 类型萃取 type traits

type traits 是模板元编程里非常实用的一组工具,用来在编译期判断、转换、组合类型信息。

最常见的几个:

  • std::is_same_v<A, B>
  • std::is_integral_v<T>
  • std::is_floating_point_v<T>
  • std::is_pointer_v<T>
  • std::remove_reference_t<T>
  • std::decay_t<T>

例子:

1
2
3
4
5
#include <type_traits>

static_assert(std::is_same_v<int, int>);
static_assert(std::is_integral_v<int>);
static_assert(std::is_pointer_v<int*>);

10.1 一个实用例子

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
#include <iostream>
#include <type_traits>
#include <vector>

template <class Func>
void fetch_data(const Func& func) {
for (int i = 0; i < 4; ++i) {
func(i);
func(i + 0.5f);
}
}

int main() {
std::vector<int> res_i;
std::vector<float> res_f;

fetch_data([&](const auto& x) {
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, int>) {
res_i.push_back(x);
} else if constexpr (std::is_same_v<T, float>) {
res_f.push_back(x);
}
});
}

这个模式的核心是:

  • decltype(x) 取表达式类型
  • std::decay_t 做常见退化
  • if constexpr 做编译期分支

11. tuple:把多个异构值打包

tuple 可以装不同类型的数据。

1
2
3
4
#include <tuple>
#include <string>

std::tuple<int, double, std::string> info{1, 3.14, "cpp"};

常见操作:

1
2
auto x = std::get<0>(info);
auto y = std::get<1>(info);

也可以结构化绑定:

1
auto [id, score, name] = info;

tuple 在模板里很重要,因为它经常和参数包、泛型封装、返回多个值一起使用。

11.1 配合 std::apply

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <tuple>
#include <utility>

int add(int a, int b, int c) {
return a + b + c;
}

int main() {
auto t = std::make_tuple(1, 2, 3);
std::cout << std::apply(add, t) << '\n';
}

12. 编译期计算与元编程

模板元编程最早常见的写法是“模板递归”。

12.1 经典例子:编译期阶乘

1
2
3
4
5
6
7
8
9
10
11
template <int N>
struct Factorial {
static constexpr int value = N * Factorial<N - 1>::value;
};

template <>
struct Factorial<0> {
static constexpr int value = 1;
};

static_assert(Factorial<5>::value == 120);

这就是典型的模板元编程:

  • 编译期递归展开
  • 编译期得到结果

但现代 C++ 中,很多时候更推荐 constexpr 函数,因为更自然、可读性更好。

12.2 更现代的写法:constexpr

1
2
3
4
5
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}

static_assert(factorial(5) == 120);

经验上可以这么记:

  • 需要泛型类型操作时,用模板
  • 需要编译期值计算时,优先考虑 constexpr

13. SFINAE 与 Concepts

这部分属于模板进阶重点。

13.1 SFINAE 是什么

SFINAE 全称:

Substitution Failure Is Not An Error

含义是:

  • 模板替换失败时,不一定报错
  • 编译器会把这个候选模板从重载集合里移除

过去常用 std::enable_if 来写约束:

1
2
3
4
5
6
7
#include <type_traits>

template <class T,
class = std::enable_if_t<std::is_integral_v<T>>>
T add_one(T x) {
return x + 1;
}

13.2 现代写法:Concepts

C++20 更推荐直接写约束:

1
2
3
4
5
6
#include <concepts>

template <std::integral T>
T add_one(T x) {
return x + 1;
}

或者:

1
2
3
4
5
template <class T>
requires std::integral<T>
T add_one(T x) {
return x + 1;
}

优势:

  • 代码更直观
  • 编译错误信息更友好
  • 语义上更接近“声明接口要求”

14. 模板与高性能的关系

模板在高性能 C++ 里不是“语法炫技”,而是重要工具。

它常被用来实现:

  • 泛型容器与算法
  • 编译期分发,避免运行时 if/switch
  • 针对不同类型或策略做静态优化
  • 内联与零成本抽象

例如一个“策略模板”:

1
2
3
4
template <class Policy>
void process(const Policy& policy) {
policy.run();
}

如果策略类型在编译期确定,编译器往往可以做更激进的内联和优化。


15. 常见坑

15.1 模板报错通常很长,先看“第一处真正失败的地方”

不要一上来被几百行错误吓住。
模板错误栈很深时,先找:

  • 第一个用户代码位置
  • 哪个类型替换失败
  • 哪个约束没满足

15.2 函数模板不能偏特化

类模板可以偏特化,函数模板不能。
函数模板通常靠:

  • 重载
  • if constexpr
  • enable_if
  • concepts

来做选择。

15.3 模板定义通常要放头文件

因为模板需要在使用点可见,编译器才能实例化。
这也是很多模板库几乎全写在头文件里的原因。

15.4 typename 的两个常见位置

第一种:声明模板类型参数

1
template <typename T>

第二种:告诉编译器“这是一个类型”

1
2
3
4
template <class T>
void f() {
typename T::value_type x{};
}

因为 T::value_type 在模板阶段不一定能立刻判断它是“类型”还是“静态成员”。


16. 运行期常量 vs 编译期常量

运行期常量:

  • 程序运行时才确定
  • 例如函数参数 int n

编译期常量:

  • 编译阶段就确定
  • 例如模板参数、constexpr 结果、static_assert 条件

区别的意义在于:

  • 编译期常量能参与模板实例化
  • 编译器可以据此裁剪分支、展开逻辑、做更多优化

17. 一页总结

模板学习的主线可以概括成:

  1. 用模板把“类型差异”参数化
  2. 用推导、特化、类型萃取处理不同类型
  3. 用参数包和 tuple 处理变长、异构数据
  4. if constexpr、SFINAE、concepts 实现静态约束和分发
  5. constexpr 与模板配合,把部分逻辑提前到编译期

如果只记几个最高频关键词:

  • 函数模板 / 类模板
  • 非类型模板参数
  • auto / decltype / 引用折叠
  • if constexpr
  • type traits
  • 参数包与 fold expression
  • 特化
  • constexpr
  • concepts

18. 建议继续补充的相关主题

如果后面继续整理,这几个主题和本文衔接最好:

  1. std::forward、完美转发与引用折叠
  2. std::move、移动语义和模板中的值类别
  3. CRTP、策略类、静态多态
  4. std::invokestd::apply、通用调用封装
  5. ranges 与 concepts 在现代泛型编程里的用法

19. 参考资料

  1. cppreference: templates
    https://en.cppreference.com/w/cpp/language/templates

  2. cppreference: type traits
    https://en.cppreference.com/w/cpp/types

  3. cppreference: fold expressions
    https://en.cppreference.com/w/cpp/language/fold

  4. cppreference: concepts
    https://en.cppreference.com/w/cpp/concepts