本章目标:函数模板、类模板、模板推断、constexpr if (C++17)、 折叠表达式 (C++17)、变参模板。模板是 C++ 区别于 Java 等的 最大武器,也是面试题重灾区。目标:会用,不钻牛角尖。
想写一个"取两个数较大值"的函数:
int max(int a, int b) { ... }
double max(double a, double b) { ... }
每个类型都抄一遍?模板就是"写给所有类型的一份代码":
template <typename T>
T max_of(T a, T b) {
return a > b ? a : b;
}
max_of(3, 5); // T = int
max_of(3.5, 2.1); // T = double
max_of(std::string("a"), std::string("b")); // T = string
编译器会为每个用到的类型"现场生成一份函数"。 人话:模板是"代码的模具",用的时候自动浇筑。
template <typename T>
T add(T a, T b) { return a + b; }
add(1, 2); // 推断 T = int
add(1, 2.5); // 两个参数类型不同 → 推断失败!编译错误
add<double>(1, 2.5); // 显式指定 T = double → 1 自动转 double
也可以多类型参数:
template <typename A, typename B>
auto mix(A a, B b) { return a + b; } // 返回类型 auto 推断
template <typename T>
class Box {
public:
Box(T value) : value_(std::move(value)) {}
const T& get() const { return value_; }
private:
T value_;
};
Box<int> b1(42);
Box<std::string> b2("hi");
模板类必须在头文件里完整定义(没有 .cpp 实现文件)—— 这是历史原因,记住这个"规矩"就行。
普通 if 在运行时判断;if constexpr 在编译期判断, false 分支的代码直接不生成(所以即使里面有编译错误也不报)。
template <typename T>
void print_thing(const T& v) {
if constexpr (std::is_pointer_v<T>) {
std::println("指针,指向 {}", *v);
} else if constexpr (std::is_same_v<T, std::string>) {
std::println("字符串 {}", v);
} else {
std::println("其他 {}", v);
}
}
为什么不用普通 if?因为普通 if 两个分支都会编译, *&v 对非指针类型是编译错误——if constexpr 则直接不生成。 这是 C++17 模板编程最常用的工具。
template <typename... Ts> // Ts 是参数包
void print_all(Ts... args) {
(std::println("{}", args), ...); // [C++17] 折叠表达式
}
print_all(1, 2.5, "three"); // 任意数量、任意类型
折叠表达式语法(先记模板,用法查表):
(expr, ...) 逗号折叠,依次执行
(... + args) 参数求和
(args + ...) 右折叠
变参模板在 std::tuple、printf 风格函数、泛型包装里无处不在。
#include <type_traits>
std::is_integral_v<int> // true:是不是整数类型
std::is_floating_point_v<double> // true
std::is_pointer_v<int*> // true
std::is_same_v<int, int> // true
std::is_const_v<const int> // true
std::remove_const_t<const int> // 变成 int
std::decay_t<T> // 去掉引用/const,得到"值类型"
配合 if constexpr 使用,就是"编译期类型判断"。
concepts 用中文说就是"约束":明确模板参数必须满足什么要求。 好处:报错信息从"几千行模板错误"变成一句话"你的类型不满足要求"。
#include <concepts>
template <std::integral T> // T 必须是整数类型
T double_it(T x) { return x * 2; }
template <typename T> requires std::integral<T> // 等价写法
T double_it2(T x) { return x * 2; }
template <std::integral T>
T sum_of(T a, T b) requires std::movable<T> { return a + b; }
标准 concept 常用:integral、floating_point、arithmetic(数值)、 same_as、derived_from、invocable(可调用)、range(范围)。
自定义 concept:
template <typename T>
concept HasArea = requires(const T& t) {
{ t.area() } -> std::same_as<double>; // 必须能调 t.area() 返回 double
};
template <HasArea T>
double total(const T& t) { return t.area(); }
requires 表达式就是"这个类型必须能这么用"的清单。 有了 concepts,模板不再是"玄学",而是"带说明书的泛型"。
经验:看第一条 error 的"最后几行",那是真正的根因; 看 file:line 的调用栈可以找到是自己哪行写错。
练习题