夜雨聆风学习资料网

ARTICLE · 1132459

C++ 函数模板(Function Template)详解

C++ 函数模板(Function Template)详解

1. 什么是函数模板?

函数模板是C++中一种强大的泛型编程工具,它允许我们编写与类型无关的通用函数。函数模板本质上是一个"函数的蓝图"或"公式",编译器可以根据使用时的具体类型,自动生成对应的函数实例。

核心思想:将函数中与类型相关的部分参数化,使得同一个算法可以适用于多种数据类型,而无需为每种类型重复编写代码。

举个直观的例子:我们想写一个返回两个数中较大值的函数。如果没有模板,需要为 int、double、char 等各写一个版本:

intmax(int a, int b){ return a > b ? a : b; }
doublemax(double a, double b){ return a > b ? a : b; }
charmax(char a, char b){ return a > b ? a : b; }

而使用函数模板,只需写一次:

template <typename T>
T max(T a, T b){
return a > b ? a : b;
}

2. 定义函数模板

函数模板的定义以关键字 template 开始,后跟用尖括号 <> 括起来的模板参数列表:

template <typename T>
返回类型 函数名(参数列表) {
// 函数体
}
  • template:声明这是一个模板
  • typename T(或 class T):声明一个类型参数 T
  • T 可以在返回类型、参数列表和函数体中使用

注意:typename 和 class 在这里等价,都表示"任意类型"参数。现代C++更推荐用 typename。

3. 函数模板的简单用法

#include<iostream>
#include<string>

template <typename T>
T myMax(T a, T b){
return a > b ? a : b;
}

intmain(){
std::cout << myMax(3, 7) << '\n';               // T 推演为 int
std::cout << myMax(3.14, 2.71) << '\n';         // T 推演为 double
std::cout << myMax(std::string("apple"),
std::string("banana")) << '\n'; // T 推演为 std::string
return0;
}

输出:

7
3.14
banana

关键点:我们只调用 myMax(...),而不用写 myMax<int>(3, 7)。编译器会根据实参类型自动推演模板参数 T。这也正是许多C++初学者在用标准库模板(如 std::sort、std::max)时,根本没意识到自己在用模板的原因——因为语法上它和普通函数调用没有区别。

4. 模板实参推演

当调用 myMax(3, 7) 时,编译器尝试让 T 满足 T = int。推演规则大致如下:

  • 若形参是 T,实参类型是 int,则 T = int。
  • 若形参是 const T&,实参是 int,则 T = int(顶层 const 被忽略)。
  • 若形参是 T&,实参是 int,则 T = int。

推演失败的情况:如果两个参数推演出不同的类型,编译器会报错:

myMax(3, 4.5);  // 错误:T 既被推为 int 又被推为 double,冲突

显式指定模板参数

可以通过 <...> 显式指定模板参数,来消除歧义:

myMax<double>(3, 4.5);   // T = double,3 被隐式转换为 double
myMax<int>(3, 4.5);      // T = int,4.5 被截断为 4,返回 4

显式指定还有另一个重要场景:当模板参数只出现在返回值中时,无法通过实参推演:

template <typename T>
T createDefault(){
return T{};
}

int x = createDefault<int>();       // 必须显式指定,否则无法推演
std::string s = createDefault<std::string>();

5. 模板的实例化与组织

编译器并不会"执行"模板,而是在遇到具体调用时,根据模板生成一个具体的函数,这个过程称为**实例化(instantiation)**。

  • 实例化是按需发生的:只有被用到的类型才会生成代码。
  • 模板定义必须对使用点可见——这就是为什么模板的定义通常放在头文件中,而不是像普通函数那样把声明放头文件、定义放源文件。
// mymax.h
#ifndef MYMAX_H
#define MYMAX_H

template <typename T>
T myMax(T a, T b){
return a > b ? a : b;
}

#endif

如果模板定义和调用分处不同翻译单元,而定义不可见,就会出现链接错误(undefined reference)。

6. 函数模板的特化

有时某个类型需要不同于通用模板的处理方式,可以为它提供特化版本。

6.1 显式特化(全特化)

#include<cstring>

template <typename T>
boolisEqual(T a, T b){
return a == b;
}

// 为 const char* 提供特化版本,用 strcmp 比较字符串内容
template <>
bool isEqual<constchar*>(constchar* a, constchar* b) {
returnstd::strcmp(a, b) == 0;
}

intmain(){
std::cout << isEqual(3, 3) << '\n';                     // 1(通用版本)
constchar* s1 = "hello";
constchar* s2 = "hello";
std::cout << isEqual(s1, s2) << '\n';                   // 1(特化版本)
return0;
}

template <> 表示这是对所有模板参数都已确定的特化,即全特化。

7. 函数模板的重载

函数模板可以重载,也可以和普通函数共存:

#include<iostream>
#include<cstring>

// 普通函数
intmyMax(int a, int b){
std::cout << "[普通函数] ";
return a > b ? a : b;
}

// 函数模板
template <typename T>
T myMax(T a, T b){
std::cout << "[模板] ";
return a > b ? a : b;
}

// 另一个重载模板:三个参数
template <typename T>
T myMax(T a, T b, T c){
return myMax(myMax(a, b), c);
}

intmain(){
    myMax(1, 2);          // 非模板优先:调用普通函数
    myMax(1.5, 2.5);      // 普通函数不匹配:用模板
    myMax(1, 2, 3);       // 三参数模板
return0;
}

重载解析原则:

  1. 先收集所有可见的候选(普通函数 + 模板特化 + 由模板生成的实例)。
  2. 进行常规的重载解析,选出最优匹配。
  3. 如果普通函数和模板实例匹配程度相同,优先选择普通函数(非模板优先)。

8. 模板定义中的名字解析

模板中的名字分为两类:

  • 依赖名字(dependent name):依赖于模板参数,在实例化时才能确定。
  • 非依赖名字(non-dependent name):不依赖模板参数,在模板定义时即被解析。
template <typename T>
voidf(T x){
int n = 0;          // 非依赖:定义时确定
    g(n);               // 若 g 是非依赖,则定义时就需要可见
typename T::value_type v;  // 依赖:实例化时确定,需加 typename 消歧
}

实践要点:

  • 依赖名字如果表示类型,需用 typename 前缀消除歧义。
  • 依赖名字中调用成员函数模板时,若需指定模板参数,要用 template 关键字:
template <typename T>
voidcall(T t){
    t.template get<int>();   // 告诉编译器 get 是模板
}

9. 名字空间中的函数模板

模板可以定义在名字空间中,推荐做法是把模板放进项目的命名空间,避免污染全局作用域:

namespace mylib {

template <typename T>
T clamp(T v, T lo, T hi){
if (v < lo) return lo;
if (v > hi) return hi;
return v;
}

} // namespace mylib

intmain(){
std::cout << mylib::clamp(15, 0, 10) << '\n';   // 10
return0;
}

注意:名字空间会影响**ADL(实参依赖查找)**。当模板的实参类型位于某个名字空间时,该名字空间中的函数也会被纳入候选集。

10. 综合示例:通用数组求和与打印

下面用一个完整的例子,把本章讲到的多个要点串起来:模板定义、实参推演、显式模板参数、特化、重载、名字空间。

#include<iostream>
#include<string>
#include<cstring>
#include<numeric>

namespace util {

// 通用求和:适用于支持 += 和默认构造的类型
template <typename T>
T sum(const T* arr, std::size_t n){
    T result{};
for (std::size_t i = 0; i < n; ++i) {
        result += arr[i];
    }
return result;
}

// 打印数组的通用版本
template <typename T>
voidprintArray(const T* arr, std::size_t n){
for (std::size_t i = 0; i < n; ++i) {
std::cout << arr[i] << (i + 1 == n ? '\n' : ' ');
    }
}

// 针对 const char* 的特化:打印字符串内容,拼接字符串
template <>
constchar* sum<constchar*>(constchar* const* arr, std::size_t n) {
staticstd::string buffer;
    buffer.clear();
for (std::size_t i = 0; i < n; ++i) {
        buffer += arr[i];
    }
return buffer.c_str();
}

} // namespace util

intmain(){
int ints[] = {1, 2, 3, 4, 5};
double dbls[] = {1.5, 2.5, 3.0};

std::cout << "整型数组: ";
    util::printArray(ints, 5);
std::cout << "求和 = " << util::sum(ints, 5) << '\n';

std::cout << "浮点数组: ";
    util::printArray(dbls, 3);
std::cout << "求和 = " << util::sum(dbls, 3) << '\n';

constchar* strs[] = {"Hello, ", "template ", "world!"};
std::cout << "字符串数组: ";
    util::printArray(strs, 3);
std::cout << "拼接 = " << util::sum(strs, 3) << '\n';

return0;
}

可能的输出:

整型数组: 1 2 3 4 5
求和 = 15
浮点数组: 1.5 2.5 3
求和 = 7
字符串数组: Hello, template world!
拼接 = Hello, template world!

小结

主题
要点
定义
template <typename T> T f(T a)
使用
多数情况下无需显式写 <T>,编译器自动推演
推演
参数类型冲突会失败,可用 <T> 显式指定
实例化
按需生成;定义需对使用点可见(通常放头文件)
特化
template <>
 为特定类型提供专门实现
重载
模板可与普通函数共存;同分时非模板优先
名字解析
依赖名字需 typename/template 消歧
名字空间
模板可置于 namespace,受 ADL 影响

函数模板是C++泛型编程的基石。掌握它之后,再学习类模板、可变参数模板、SFINAE、Concepts 等高级主题就会水到渠成。建议读者从改写手头的"重复函数"开始,亲手把若干重载函数合并成一个模板,体会泛型编程带来的简洁与威力。

相关学习资料