代码仓库shanchuann/CPP-Learninng

函数模板被称为”生成代码的代码”,是 C++ 通用编程的核心工具,它通过参数化类型定义一族函数的蓝图,编译时会根据实际使用的类型 / 值自动生成具体函数实例,无需为每种场景重复编写相同逻辑,大幅提升代码复用性与扩展性。

函数模板的声明 / 定义需遵循特定语法,C++ 标准演进中新增了简化语法,同时废弃了部分特性,核心语法分为以下几类:

函数模板的语法

标准语法(C++98 起)

基础语法通过template关键字声明模板参数列表,后接函数声明 / 定义,适用于所有 C++ 标准。

1
2
3
4
5
6
7
8
9
10
11
12
// 语法格式:template <参数列表> 函数声明/定义
template <typename T> // T为类型模板参数,typename可替换为class
T max(T a, T b) {
return a > b ? a : b;
}

template <int N, typename T> // N为非类型模板参数,T为类型参数
void fillArray(T arr[]) {
for (int i = 0; i < N; i++) {
arr[i] = T(); // 初始化T类型的默认值
}
}

C++20 新增语法

C++20 引入约束(constraints)缩写函数模板,简化模板定义并增强类型检查。

  • 带约束的模板:用requires限定模板参数需满足的条件(依赖概念库)

    1
    2
    3
    4
    5
    template <typename T>
    requires std::integral<T> // 约束T为整数类型
    T add(T a, T b) {
    return a + b;
    }
  • 缩写函数模板:用autoConcept auto作为参数类型占位符,隐式生成模板参数

    1
    2
    3
    4
    5
    6
    7
    void print(auto value) {  // 等价于template <typename T> void print(T value)
    std::cout << value << std::endl;
    }

    void sum(std::floating_point auto a, std::floating_point auto b) { // 约束为浮点类型
    std::cout << a + b << std::endl;
    }

废弃语法

export关键字(C++11 前允许,C++11 起移除)曾用于声明 “导出模板”,允许跨文件实例化,但因实现复杂且兼容性差被废弃,现无需使用。

模板实参的推演

在编译时期,函数模板通过使用typedef关键字对不同类型的参数进行重命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
template <class T>
T Max(const T& a, const T& b) {
return (a > b) ? a : b;
}
int main() {
int x = 10, y = 20;
double p = 10.5, q = 20.5;
char c1 = 'A', c2 = 'B';
cout << "Max of " << x << " and " << y << " is: " << Max(x, y) << endl;
cout << "Max of " << p << " and " << q << " is: " << Max(p, q) << endl;
cout << "Max of " << c1 << " and " << c2 << " is: " << Max(c1, c2) << endl;
return 0;
}


typedef int T;
T Max<int>(const T& a, const T& b) {
return (a > b) ? a : b;
}
typedef double T;
T Max<double>(const T& a, const T& b) {
return (a > b) ? a : b;
}
typedef char T;
T Max<char>(const T& a, const T& b) {
return (a > b) ? a : b;
}

函数模板根据提供的实参推演出形参的类型,生成不同的函数进行编译

含不同类型实参的模板

1
2
3
4
5
6
7
8
9
10
template <class T,class U>
auto Max(const T& a, const U& b) {
return (a > b) ? a : b;
}
int main() {
int x = 10;
double y = 20.5;
cout << "Max of " << x << " and " << y << " is: " << Max(x, y) << endl;
return 0;
}

C++17后引入auto的概念,使得返回类型可以自行推演

  • 优势:通过模板生成对应函数,泛型编程
  • 劣势:类型名越多,生成的代码越多,导致代码体量膨胀

模板参数类型

函数模板的参数列表支持三类参数,可组合使用:

  • 类型参数:用typename/class声明,代表一个未指定的类型(如template <typename T>中的T)。
  • 非类型参数:代表一个编译期已知的常量值,类型需为整数类型、枚举、指针或引用(如template <int N>中的N)。
  • 参数包(C++11 起):用...声明,可接受任意数量的同类型参数(如template <typename... Args>中的Args),配合折叠表达式使用。

编译时实例化

函数模板本身不生成代码,仅在 “需要使用” 时由编译器生成具体实例(实例化),分为显式实例化和隐式实例化两种方式。

  • 实例化过程:编译器根据传入的模板实参,替换模板中的参数占位符,生成独立的函数代码(如max<int>(1,2)生成int max(int a, int b))。
  • 特点:实例化发生在编译期,无运行时开销,且类型错误可在编译期发现。

默认模板参数

C++11 起允许为模板参数指定默认值,使用时若未显式传入实参,则采用默认值。

1
2
3
4
5
6
7
template <typename T = int, int Size = 10>  // T默认int,Size默认10
class Array {
T data[Size];
};

Array<> arr1; // 等价于Array<int, 10>
Array<double, 5> arr2; // 显式指定参数

函数模板的实例化

实例化是将模板转化为具体函数的过程,根据触发方式分为显式实例化和隐式实例化。

显式实例化

开发者主动指定模板实参,强制编译器生成实例,适用于需控制实例化位置(如减少重复编译)的场景。

  • 显式实例化定义:生成实例代码,程序中同一参数列表的实例只能定义一次。

    1
    2
    3
    4
    5
    template <typename T> T max(T a, T b) { return a > b ? a : b; }

    // 显式实例化max<int>
    template int max<int>(int a, int b);
    template int max<>(int a, int b); // 实参可推导时,<>内可省略
  • 显式实例化声明(C++11 起):用extern声明 “实例在其他文件中定义”,阻止当前文件隐式实例化,避免重复生成代码。

    1
    2
    3
    4
    5
    // a.h中声明
    extern template int max<int>(int a, int b);

    // a.cpp中定义
    template int max<int>(int a, int b);

隐式实例化

编译器在 “需要使用模板函数” 时自动推导实参并生成实例,无需开发者干预,是最常用的实例化方式。

  • 触发场景:函数调用、取函数地址、初始化函数指针等。

    1
    2
    3
    4
    5
    6
    7
    8
    template <typename T> void print(T value) { std::cout << value << std::endl; }

    int main() {
    print(10); // 隐式实例化print<int>
    print(3.14); // 隐式实例化print<double>
    void (*fp)(std::string) = print; // 隐式实例化print<std::string>
    return 0;
    }
  • 注意:C++11 起,若表达式需常量求值(如constexpr函数),即使未直接调用,也可能触发隐式实例化。

模板实参推导与替换

模板实参推导

编译器根据函数调用的实参类型,自动推导模板参数的过程,无需显式指定所有实参(部分场景需显式指定,如返回类型无法推导)。

  • 推导规则:实参类型需与模板参数类型 “兼容”,忽略顶层const/volatile,数组和函数类型会退化为指针。

    1
    2
    3
    4
    5
    6
    7
    template <typename To, typename From> To convert(From f) { return static_cast<To>(f); }

    int main() {
    int i = convert<int>(3.14); // From推导为double,To显式指定为int
    char c = convert<char>(65); // From推导为int,To显式指定为char
    return 0;
    }
  • 运算符模板的推导:重载运算符(如<<)无法显式指定模板实参,需依赖推导(如std::cout << "hello"推导operator<<<char>)。

模板实参替换

推导得到模板实参后,编译器将模板中的参数占位符替换为具体实参,生成实例代码。

  • 替换失败:若替换后代码非法(如调用不存在的成员函数),编译器不会报错,而是将该模板从重载集中移除,此机制称为SFINAE(替换失败不是错误),是模板元编程的基础。

    1
    2
    3
    4
    // 仅当T有size()成员函数时,此模板才有效(否则替换失败,被移除)
    template <typename T> auto getSize(const T& t) -> decltype(t.size(), void()) {
    std::cout << t.size() << std::endl;
    }

函数模板的重载

函数模板可与非模板函数、其他函数模板重载,编译器通过重载决议选择最佳匹配,核心规则如下:

重载的形式

  • 模板与非模板重载:非模板函数优先于模板特化(若类型完全匹配)。

    1
    2
    3
    4
    5
    6
    7
    8
    void print(int x) { std::cout << "非模板:" << x << std::endl; }
    template <typename T> void print(T x) { std::cout << "模板:" << x << std::endl; }

    int main() {
    print(10); // 调用非模板函数(完全匹配)
    print(3.14); // 调用模板函数(推导为double)
    return 0;
    }
  • 多模板重载:通过部分排序判断 “更特化” 的模板,更特化的模板优先匹配(“更特化” 指接受的类型范围更小,如T*T更特化)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    template <typename T> void f(T x) { std::cout << "T" << std::endl; }
    template <typename T> void f(T* x) { std::cout << "T*" << std::endl; }

    int main() {
    int a = 10;
    f(a); // 调用f(T)(T推导为int)
    f(&a); // 调用f(T*)(T推导为int,更特化)
    return 0;
    }

部分排序规则

判断两个模板 “谁更特化” 的核心逻辑:将一个模板的参数 “虚构类型” 代入另一个模板,若能推导成功,则后者更通用,前者更特化。

  • 示例:比较f(T)f(T*)
    1. f(T*)的虚构类型U*代入f(T),推导T=U*,成功 → f(T)更通用。
    2. f(T)的虚构类型U代入f(T*),推导T*=U,失败 → f(T*)更特化。
    3. 结论:f(T*)优先于f(T)匹配指针类型实参。

函数模板的特化

函数模板特化是为特定模板实参,提供定制化的实现(区别于重载,特化不是新增函数,而是为已有模板提供 “特殊实例”)。

显式特化

为特定模板实参显式定义实现,语法为template<> 函数声明

1
2
3
4
5
6
7
8
9
10
11
12
13
// 主模板
template <typename T> void print(T x) { std::cout << "通用:" << x << std::endl; }

// 为int类型显式特化
template <> void print<int>(int x) {
std::cout << "特化(int):" << x << std::endl;
}

int main() {
print(3.14); // 调用主模板(double)
print(10); // 调用显式特化(int)
return 0;
}

特化与重载的区别

  • 特化:不改变模板的 “家族关系”,仅为特定实参定制实现,需依赖主模板存在。
  • 重载:新增独立的函数(模板或非模板),与原模板无 “从属关系”,重载决议时独立参与匹配。
  • 关键注意:编译器先通过重载决议选择主模板,再检查该主模板是否有特化,而非直接选择特化。