第十章 模板与泛型:写一次,用所有类型

← 返回目录

本章目标:函数模板、类模板、模板推断、constexpr if (C++17)、 折叠表达式 (C++17)、变参模板。模板是 C++ 区别于 Java 等的 最大武器,也是面试题重灾区。目标:会用,不钻牛角尖。

10.1 为什么需要模板?

想写一个"取两个数较大值"的函数:

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

编译器会为每个用到的类型"现场生成一份函数"。 人话:模板是"代码的模具",用的时候自动浇筑。

10.2 模板参数推断与显式指定

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 推断

10.3 类模板:容器就是这么来的

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 实现文件)—— 这是历史原因,记住这个"规矩"就行。

10.4 [C++17] if constexpr:编译期的 if

普通 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 模板编程最常用的工具。

10.5 变参模板(...)

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 风格函数、泛型包装里无处不在。

10.6 类型特性(type traits):编译期问问题

#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 使用,就是"编译期类型判断"。

10.7 [C++20] concepts:给模板加"要求"(最优雅的模板控制)

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,模板不再是"玄学",而是"带说明书的泛型"。

10.8 模板何时实例化?报错为什么那么长?

经验:看第一条 error 的"最后几行",那是真正的根因; 看 file:line 的调用栈可以找到是自己哪行写错。

本章小结

练习题


  1. 写模板 min_of、abs_of(通用绝对值,注意处理无符号)。
  2. 写模板类 Pair<A,B>,存两个任意类型的值,带 get。
  3. 用 if constexpr + is_integral 写函数:整数打印"整数",否则"其他"。
  4. 写变参模板 sum_all(...args) 用折叠表达式求和。
  5. 给 9.6 的 Shape 加一个 concept HasArea,写个总面积的函数模板。