函数模板实例化

在上节课(函数模板)中,我们介绍了函数模板,并把普通的 max 函数改写为模板:

template <typename T>
T max(T x, T y)
{
    return (x < y) ? y : x;
}

本课将聚焦于函数模板的使用方式。

使用函数模板

函数模板本身并非真正的函数——其代码不会被直接编译或执行。它的唯一职责是生成函数(这些生成的函数才会被编译和执行)。

调用 max<T> 模板需使用如下语法:

max<实际类型>(arg1, arg2); // 实际类型如 int 或 double

与常规函数调用几乎相同,区别只是在尖括号中指定模板实参,告诉编译器用何种具体类型替换模板形参 T

示例:

#include <iostream>

template <typename T>
T max(T x, T y)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max<int>(1, 2) << '\n'; // 实例化并调用 max<int>(int, int)
    return 0;
}

当编译器遇到 max<int>(1, 2) 时,发现尚未定义 max<int>(int, int),于是隐式地利用模板生成该函数。

从模板生成具体类型函数的过程称为函数模板实例化(instantiation)。由函数调用触发的实例化称为隐式实例化。生成的函数称为特化(specialization),俗称函数实例;原模板则称为主模板。实例化后的函数与普通函数完全一致。

术语说明
“特化”一词更常指显式特化,即手动为模板定义特定版本的函数(见 26.3 课)。

实例化流程简单描述:编译器复制主模板,将模板形参 T 替换为指定的具体类型(如 int)。

调用 max<int>(1, 2) 时,生成的特化大致如下:

template<>
int max<int>(int x, int y)
{
    return (x < y) ? y : x;
}

完整编译后,程序等价于:

#include <iostream>

template <typename T>
T max(T x, T y);

template<>
int max<int>(int x, int y)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max<int>(1, 2) << '\n';
    return 0;
}
  • 每个翻译单元中,模板在首次调用时实例化一次;后续调用复用已生成的函数。
  • 若无调用,则该翻译单元不会实例化该模板。

再举一例:

#include <iostream>

template <typename T>
T max(T x, T y)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max<int>(1, 2) << '\n';    // 实例化 max<int>
    std::cout << max<int>(4, 3) << '\n';    // 复用 max<int>
    std::cout << max<double>(1, 2) << '\n'; // 实例化 max<double>
    return 0;
}

最终程序等价于:

// 模板声明
template <typename T>
T max(T x, T y);

// 生成的特化
template<>
int max<int>(int x, int y) { ... }

template<>
double max<double>(double x, double y) { ... }

注意:实例化 max<double> 时形参类型为 double,而我们传入 int,实参会被隐式转换为 double

模板实参推导

实参类型希望实例化的类型一致时,可省略显式指定,让编译器根据实参类型推导模板实参:

std::cout << max<int>(1, 2);   // 显式指定
std::cout << max<>(1, 2);      // 仅考虑模板,推导为 max<int>
std::cout << max(1, 2);        // 普通调用语法,推导为 max<int>
  • max<>(1, 2):仅考虑模板重载,推导 max<int>
  • max(1, 2):同时考虑模板和非模板重载;若两者同样匹配,优先非模板函数

示例:

#include <iostream>

template <typename T>
T max(T x, T y)
{
    std::cout << "max<T>\n";
    return (x < y) ? y : x;
}

int max(int x, int y)
{
    std::cout << "max(int,int)\n";
    return (x < y) ? y : x;
}

int main()
{
    max<int>(1, 2); // 模板
    max<>(1, 2);    // 模板(非模板不参与)
    max(1, 2);      // 非模板优先
}

最佳实践
除非需要强制使用模板版本,优先使用普通函数调用语法

模板与普通形参混用

模板也可含非模板形参:

template <typename T>
int someFcn(T, double) // double 为非模板形参
{
    return 5;
}

调用示例:

someFcn(1, 3.4);    // T=int, double 已匹配
someFcn(1.2f, 3.4); // float 提升为 double,T=float

实例化后的函数可能无法编译

模板只要语法合法即可实例化,但语义是否合理需程序员保证。

示例:

template <typename T>
T addOne(T x) { return x + 1; }

int main()
{
    addOne("Hello"); // 编译 OK,但语义错误(指针偏移)
}

可显式禁止某些实例化:

template <>
const char* addOne<const char*>(const char*) = delete;

静态局部变量与模板

模板函数中的 static 局部变量,每个实例化版本各有一份:

template <typename T>
void printID(T v)
{
    static int id = 0;
    std::cout << ++id << ") " << v << '\n';
}

printID(12);   // id=1(int版)
printID(13);   // id=2(int版)
printID(14.5); // id=1(double版)

泛型编程

因模板类型可替换为任意实际类型,又称泛型编程(generic programming)。它让我们聚焦算法与数据结构,而非具体类型。

结论

函数模板写起来与普通函数几乎一样,却能显著减少重复代码与维护成本。
缺点:

  • 每遇到新类型组合就生成一份函数,可能导致代码膨胀编译时间增加
  • 错误信息往往冗长难读。

权衡之下,优势远大于缺点。建议:先写普通函数,发现需要不同参数类型时再升级为模板。

最佳实践

需要跨类型通用算法时,优先使用函数模板。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"