函数模板

定义模板

函数模板不是函数,只有实例化函数模板,编译器才能生成实际的函数定义。

声明一个函数模板,我们通常要使用:

template< 形参列表 > 函数声明

[!tip]

C++17 之前,类型 T 必须是可复制或移动才能传递参数。C++17 以后,即使复制构造函数和移动构造函数都无效,因为 C++17 强制的复制消除,也可以传递临时纯右值。

也可以使用 class 关键字来声明模板类型形参

使用模板


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

struct Test{
    int v_{};
    Test() = default;
    Test(int v) :v_(v) {}
    bool operator>(const Test& t) const{
        return this->v_ > t.v_;
    }
};

int main(){
    int a{ 1 };
    int b{ 2 };
    std::cout << "max(a, b) : " << ::max(a, b) << '\n';

    Test t1{ 10 };
    Test t2{ 20 };
    std::cout << "max(t1, t2) : " << ::max(t1, t2).v_ << '\n';

}

编译器会实例化两个函数,也就是生成了一个参数为 int 的 max 函数,一个参数为 Test 的函数。

例如

int max(int a, int b)
{
  return a > b ? a : b;
}

Test max(Test a, Test b)
{
  return a > b ? a : b;
}
  • 模板,只有你“用”了它,才会生成实际的代码

这里的“”,其实就是指代会隐式实例化,生成代码。

并且需要注意,同一个函数模板生成的不同类型的函数,彼此之间没有任何关系。

[!tip]

显式的指明函数模板的形参类型

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

int main(){
    int a{ 1 };
    int b{ 2 };
    max(a, b);          // 函数模板 max 被推导为 max<int>

    max<double>(a, b);  // 传递模板类型实参,函数模板 max 为 max<double>
}

模板参数推导

当使用函数模板(如 max())时,模板参数可以由传入的参数推导。

T 可能只是类型的“一部分”。若声明 max() 使用 const&

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

如果我们 max(1, 2) 或者说 max<int>(x,x),T 当然会是 int,但是函数形参类型会是 const int&。详见Deducing Types

[!warning]

有不少情况是没有办法进行推导的:即参数有多个类型并且没有显示指定类型

// 省略 max
using namespace std::string_literals;
int main(){
    max(1, 1.2);            // Error 无法确定你的 T 到底是要 int 还是 double
    max("luse"s, "乐");     // Error 无法确定你的 T 到底是要 std::string 还是 const char[N]
}

需要显式指定函数模板的(T)类型

max<double>(1, 1.2);           
max<std::string>("luse"s, "乐");

又或者说显式类型转换

max(static_cast<double>(1), 1.2);

万能引用与引用折叠

所谓的万能引用(又称转发引用,或者通用引用),即接受左值表达式那形参类型就推导为左值引用,接受右值表达式,那就推导为右值引用

[!tip]

通用引用不是一种新的引用,它实际上是满足以下两个条件下的右值引用

  • 类型推导区分左值和右值T类型的左值被推导为T&类型,T类型的右值被推导为T
  • 发生引用折叠

比如:

template<typename T>
void f(T&& t){}

int a = 10;
f(a);       // a 是左值表达式,f 是 f<int&> 但是它的形参类型是 int&
f(10);      // 10 是右值表达式,f 是 f<int> 但它的形参类型是 int&&

被推导为 f<int&> 涉及到了特殊的推导规则:如果 P 是到无 cv 限定模板形参的右值引用(也就是转发引用)且对应函数的调用实参是左值,那么将到 A 的左值引用类型用于 A 的位置进行推导。

通过模板或 typedef 中的类型操作可以构成引用的引用,此时适用引用折叠(reference collapsing)规则:

  • 右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用
typedef int&  lref;
typedef int&& rref;
int n;

lref&  r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref&  r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
template <class Ty>
constexpr Ty&& forward(Ty& Arg) noexcept {
    return static_cast<Ty&&>(Arg);
}

int a = 10;            // 不重要
::forward<int>(a);     // 返回 int&& 因为 Ty 是 int,Ty&& 就是 int&&
::forward<int&>(a);    // 返回 int& 因为 Ty 是 int&,Ty&& 就是 int&
::forward<int&&>(a);   // 返回 int&& 因为 Ty 是 int&&,Ty&& 就是 int&&

有默认实参的模板类型形参

模板形参也可以有默认值

template<typename T = int>
void f();

f();            // 默认为 f<int>
f<double>();    // 显式指明为 f<double>
using namespace std::string_literals;

template<typename T1,typename T2,typename RT = 
    decltype(true ? T1{} : T2{}) >

RT max(const T1& a, const T2& b) { // RT 是 std::string
    return a > b ? a : b;
}

int main(){
    auto ret = ::max("1", "2"s);
    std::cout << ret << '\n';
}

[!important]

typename RT = decltype(true ? T1{} : T2{})

RT声明为三目运算符表达式的类型

[!tip]

三目表达式要求第二项和第三项之间能够隐式转换,然后整个表达式的类型会是 “公共”类型

比如第二项是 int 第三项是 double,三目表达式当然会是 double。

using T = decltype(true ? 1 : 1.2);
using T2 = decltype(false ? 1 : 1.2);

T 和 T2 都是 double 类型

利用auto 简化这一切。

template<typename T,typename T2>
auto max(const T& a, const T2& b) -> decltype(true ? a : b){
    return a > b ? a : b;
}

C++11 后置返回类型

[!warning]

后置返回类型虽然也是写的 auto ,但是它根本没推导,只是占位

这和我们之前用默认模板实参 RT 的区别的返回类型是不一样的

如果函数模板的形参是类型相同 true ? a : b 表达式的类型是 const T&

使用 C++20 简写函数模板,我们可以直接再简化为:

decltype(auto) max(const auto& a, const auto& b)  {
    return a > b ? a : b;
}

C++14 引入了两个特性:

  1. 返回类型推导(也就是函数可以直接写 auto 或 decltype(auto) 做返回类型,而不是像 C++11 那样,只是后置返回类型。
  2. decltype(auto)如果返回类型没有使用 decltype(auto),那么推导遵循模板实参推导的规则进行”。我们上面的 max 示例如果不使用 decltype(auto),按照模板实参的推导规则,会忽略引用和 cv 限定符,就只能推导出返回 T 基本类型。

非类型模板形参

模板不接受类型,而是接受值或对象

非类型模板形参当然也可以有默认值:

template<std::size_t N = 100>
void f() { std::cout << N << '\n'; }

f();     // 默认      f<100>
f<66>(); // 显式指明  f<66>

目前,你简单认为需要参数是“常量”即可。


重载函数模板

函数模板与非模板函数可以重载。

template<typename T>
void test(T) { std::puts("template"); }

void test(int) { std::puts("int"); }

test(1);        // 匹配到test(int)
test(1.2);      // 匹配到模板
test("1");      // 匹配到模板
  • 通常优先选择非模板的函数。

可变参数模板

[!important]

形参包

本节以 C++14 标准进行讲述。

模板形参包是接受零个或更多个模板实参(非类型、类型或模板)的模板形参。函数形参包是接受零个或更多个函数实参的函数形参。

template<typename...Args>
void sum(Args...args){}

这样一个函数,就可以接受任意类型的任意个数的参数调用

模板中需要 typename 后跟三个点 Args,函数形参中需要用模板类型形参包后跟着三个点 再 args。

args 是函数形参包,Args 是类型形参包,它们的名字我们可以自定义。

args 里,就存储了我们传入的全部的参数,Args 中存储了我们传入的全部参数的类型。

形参包展开

void f(const char*, int, double) { puts("值"); }
void f(const char**, int*, double*) { puts("&"); }

template<typename...Args>
void sum(Args...args){  // const char * args0, int args1, double args2
    f(args...);   // 相当于 f(args0, args1, args2)
    f(&args...);  // 相当于 f(&args0, &args1, &args2)
}

int main() {
    sum("luse", 1, 1.2);
}

sum 的 Args...args 被展开为 const char * args0, int args1, double args2

[!tip]

定义一个术语:模式

后随省略号且其中至少有一个形参包的名字的模式会被展开 成零个或更多个逗号分隔的模式实例。

  • &args...&args 就是模式

    展开的时候,模式,也就是省略号前面的一整个表达式,会被不停的填入对象并添加 &,然后逗号分隔。直至形参包的元素被消耗完。

    template<typename...Args>
    void print(const Args&...args){    // const char (&args0)[5], const int & args1, const double & args2
        int _[]{ (std::cout << args << ' ' ,0)... };
    }
    
    int main() {
        print("luse", 1, 1.2);
    }
    

    模式是:(std::cout << args << ' ' ,0)

[!warning]

  • 只有在合适的形参包展开场所才能进行形参包展开
template<typename ...Args>
void print(const Args &...args) {
   (std::cout << args << " ")...; // 不是合适的形参包展开场所 Error!
}

一个数组的示例:

template<typename...Args>
void print(const Args&...args) {
    int _[]{ (std::cout << args << ' ' ,0)... };
}

//onst T(&array)[N] 注意,这是一个数组引用
template<typename T,std::size_t N, typename...Args>
void f(const T(&array)[N], Args...index) {
    print(array[index]...); //array[index]... 是包展开
                            //array[index] 是模式
}

int main() {
    int array[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    f(array, 1, 3, 5);
}
  • 实现一个 sum

    #include <iostream>
    #include <type_traits>
    
    template<typename...Args,typename RT = std::common_type_t<Args...>>
    RT sum(const Args&...args) {
        RT _[]{ static_cast<RT>(args)... };
        RT n{};
        for (int i = 0; i < sizeof...(args); ++i) { // sizeof... 获取形参包的元素个数
            n += _[i];
        }
        return n;
    }
    
    int main() {
        double ret = sum(1, 2, 3, 4, 5, 6.7);
        std::cout << ret << '\n';       // 21.7
    }
    
  • std::common_type_t 的作用很简单,就是确定我们传入的共用类型,说白了就是这些东西都能隐式转换到哪个,那就会返回那个类型。

  • [!tip]

    RT _[]{ static_cast<RT>(args)... }; 创建一个数组,形参包在它的初始化器中展开,初始化这个数组,数组存储了我们传入的全部的参数。

    因为窄化转换禁止了列表初始化中 int 到 double 的隐式转换,所以我们需要显式的转换为“公共类型” RT

  • 非类型模板形参也可以使用形参包

    template<std::size_t... N>
    void f(){
        std::size_t _[]{ N... }; // 展开相当于 1UL, 2UL, 3UL, 4UL, 5UL
        std::for_each(std::begin(_), std::end(_), 
            [](std::size_t n){
                std::cout << n << ' ';
            }
        );
    }
    f<1, 2, 3, 4, 5>();
    

模板分文件

对模板进行分文件,写成 .h .cpp 这种形式。

这显然是不可以的

#include "test.h"
#include "test_template.h"

int main(){
    f();    // 非模板,OK
    f_t(1); // 模板 链接错误
}

从头讲解编译链接,以及 #include 的知识

include 指令

预处理指令 #include 开始,就是简单的替换,但是不够明确

分文件的原理是什么?

通常将函数声明放在 .h 文件中,将函数定义放在 .cpp 文件中,我们只需要在需要使用的文件中 include 一个 .h 文件

事实上是把函数声明复制到了我们当前的文件中。

//main.cpp
#include "test.h"

int main(){
    f();    // 非模板,OK
}

编译器在编译一个翻译单元(如 main.cpp)的时候,如果发现找不到函数的定义,那么就会空着一个符号地址,将它编译为目标文件。期待链接器在链接的时候去其他的翻译单元找到定义来填充符号。

不单单是函数,全局变量等都是这样,这是编译链接的基本原理和步骤

类会有所不同,总而言之后续视频会单独讲解的。

不能模板不能分文件4的原因就显而易见了,我们在讲使用模板的时候就说了:

  • 模板,只有你“用”了它,才会生成实际的代码

单纯的放在一个 .cpp 文件中,它不会生成任何实际的代码,自然也没有函数定义,也谈不上链接器找符号了。

所以模板通常是直接放在 .h 文件中,而不会分文件。或者说用 .hpp 这种后缀,这种约定俗成的,代表这个文件里放的是模板。


  1. 注:函数模板自身并不是类型、函数或任何其他实体。不会从只包含模板定义的源文件生成任何代码。模板只有实例化才会有代码出现。
  2. 注:术语“实例化”,指代的是编译器确定各模板实参(可以是根据传入的参数推导,又或者是自己显式指明模板的实参)后从模板生成实际的代码(如从函数模板生成函数,类模板生成类等),这是在编译期就完成的,没有运行时开销。实例化还分为隐式实例化和显式实例化,后面会详细聊。
  3. 注:“重载决议”,简单来说,一个函数被重载,编译器必须决定要调用哪个重载,我们决定调用的是各形参与各实参之间的匹配最紧密的重载。
  4. 注:这个问题可以通过显式实例化解决,在后面会讲。 [↩](https://github.com/Mq-b/Modern-Cpp-templates-tutorial/blob/main/md/第一部分-基础知识/01函数模板.md#user-content-fnref-4-3aff5140d04d84f7bex

©OZY all right reserved该文件修订时间: 2025-09-20 05:42:10

评论区 - 01_函数模板

results matching ""

    No results matching ""