跳至内容

函数模板与类型推导

学习如何编写适用于任何类型的函数。理解模板参数推导、非类型参数,以及为什么模板需要放在头文件中。

为什么需要函数模板?

如果没有模板,编写一个适用于多种类型的函数意味着重复逻辑。你需要编写 int max(int, int)、double max(double, double)、std::string max(std::string, std::string) ── 为每种类型重复相同的逻辑。函数模板让你只编写一次算法,然后让编译器自动生成特定类型的版本。

函数模板不是函数 ── 它是一个蓝图,编译器在你使用特定类型时从中实例化实际的函数。这完全发生在编译时,生成的代码与你手写的完全相同 ── 零开销。

模板语法

函数模板以 template 关键字开头,后跟尖括号中的参数列表。你可以互换使用 typename 或 class 作为类型参数 ── 它们的含义完全相同。现代惯例更倾向于使用 typename。

function_template.cpp
#include <iostream>
#include <string>

// 'typename' and 'class' are interchangeable here
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Equivalent — 'class' means the same thing
template <class T>
T minimum(T a, T b) {
    return (a < b) ? a : b;
}

int main() {
    std::cout << maximum(3, 7)          << '\n'; // deduces T = int
    std::cout << maximum(3.14, 2.71)    << '\n'; // deduces T = double
    std::cout << maximum<std::string>("hello", "world") << '\n';
    // explicit instantiation needed — "hello" is const char*, not std::string
}

模板参数推导规则

编译器通过将函数参数的类型与参数模式进行匹配来推导模板参数。关键规则如下:

1. 优先精确匹配 ── maximum(3, 7) 推导出 T = int,因为两个参数都是 int。
2. 推导过程中不进行隐式转换 ── maximum(3, 7.0) 是一个错误:编译器从第一个参数推导出 T = int,从第二个参数推导出 T = double,然后拒绝选择其中一个。
3. 数组到指针的退化 ── 当 T 作为值参数出现时,数组退化为指针。使用 T(&)[N] 来捕获数组类型和大小。
4. 顶层 const/volatile 被剥离 ── 对于值参数会剥离,但对于引用/指针参数则保留。
5. 引用折叠 ── 如果 T 被推导为引用类型(在转发引用中),引用折叠规则适用:T& & 变为 T&,T& && 变为 T&,T&& & 变为 T&,T&& && 变为 T&&。

当推导失败或产生歧义时,你必须显式提供模板参数:maximum(3, 7.0)。

非类型模板参数

模板参数不限于类型。非类型模板参数(NTTP)接受编译时值,如整数、指针,或者(自 C++20 起)浮点数和类类型。它们在将编译时常量嵌入类型系统方面非常强大。

non_type_params.cpp
#include <iostream>
#include <array>
#include <cstddef>

// Non-type parameter: N is a compile-time size_t value
template <typename T, std::size_t N>
T sum(const std::array<T, N>& arr) {
    T total{};
    for (const auto& elem : arr) {
        total += elem;
    }
    return total;
}

// C++20: auto non-type parameter accepts any structural type
template <auto Value>
constexpr auto doubled = Value * 2;

int main() {
    std::array<int, 4> nums = {1, 2, 3, 4};
    std::cout << sum(nums) << '\n';     // 10, deduces T=int, N=4

    std::cout << doubled<21> << '\n';   // 42
    std::cout << doubled<3.14> << '\n'; // 6.28 (C++20)
}
提示

每种唯一的模板参数组合都会导致编译器生成一个独立的实例化 ── 在二进制文件中生成该函数的一份独立副本。调用 maximum、maximum 和 maximum 会产生三个函数。这通常没问题,但在大型代码库中,模板可能导致代码膨胀(更大的二进制文件、更长的编译时间)。缓解策略包括:使用 extern template 声明来控制实例化位置,将与类型无关的代码从模板中提取出来,以及在运行时多态可接受时使用类型擦除。

常见陷阱

因为编译器需要看到完整的模板定义才能实例化它,所以模板不能像普通函数那样拆分为 .h / .cpp 文件。如果你将模板定义放在 .cpp 文件中,然后尝试从另一个翻译单元使用它,你会得到一个链接器错误(未定义引用)。标准解决方案是将整个模板定义放在头文件中。如果编译时间成为问题,你可以使用显式实例化:将定义放在 .cpp 文件中,然后添加 template int maximum(int, int); 来显式实例化你需要的特化版本。

显式实例化

你可以显式控制模板在何时何地被实例化。这可以通过防止跨翻译单元的冗余实例化来减少大型项目的编译时间。

explicit_instantiation.h
// ---- maximum.h ----
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Suppress implicit instantiation in every TU that includes this header
extern template int maximum<int>(int, int);
extern template double maximum<double>(double, double);

// ---- maximum.cpp ----
#include "maximum.h"

// Explicit instantiation: generate code here, once
template int maximum<int>(int, int);
template double maximum<double>(double, double);
关键要点
  • 函数模板是蓝图 ── 编译器为每组模板参数生成具体的函数
  • typename 和 class 在模板参数列表中是可互换的
  • 模板参数推导不会进行隐式转换 ── 有歧义的推导会产生错误
  • 非类型模板参数将编译时值嵌入类型系统
  • 模板必须在头文件中定义(或使用显式实例化),因为编译器需要完整定义才能实例化

测验 — 检验你的知识

(15 经验值)

1. 当你使用模板 `template<typename T> T maximum(T a, T b)` 调用 `maximum(3, 7.0)` 时,会发生什么?

2. 为什么模板定义通常必须放在头文件中?

3. `extern template int maximum<int>(int, int);` 的用途是什么?