C++PrimerPlus 学习笔记 | 第八章 函数探幽 |5.函数模版

本文介绍了C++中的函数模板,展示了如何通过模板编写通用的swap函数,包括模板的使用、重载、显式具体化和实例化的过程,以及编译器选择函数版本的规则。讨论了模版在不同类型间的灵活性和局限性,以及如何通过自定义选择来引导编译器行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

函数模版

函数模版是通用函数的描述。也就是说使用范型来定义函数,其中的范型可以用于具体类型的类型进行替换(如int或者double,或者是任何的用户自定义类型),通过将类型参数传递给模版,可以使得编译器生成该类型的函数。由于模版允许以范型(而非具体的类型)的方式来编写程序,因此也可以称为通用编程.由于类型是由参数表示的,有时候模版也被称为参数化类型。

引入:swap函数

我们可以通过引用变量很轻松的写出一个int类型或者double类型的交换函数,如下

void swap(int & a, int & b)
{
    int t = a;
    a = b;
    b = t;
}
void swap(double & a, double & b)
{
    double t = a;
    a = b;
    b = t;
}

但是如果我们遇到float类型怎么办,当然我们也可以通过函数重载的方式写出所有基本的类型的交换函数,那么对于用户自定义函数呢?我们不可能写出一切的用户自定义类型,我们不知道也无法知道用户会定义怎么样的类型,但是我们会发现对于所有的交换函数例如int型我们只需要将其中int -> double 就可以变为double型的交换函数,那么我们能否借助函数模版来自动生成呢?幸运的是C++的函数模版功能可以自动完成这一功能,可以节省时间而且更可靠。

函数模版允许以任意类型的方式来定义函数 我们看一个交换模版

// 建立模版必须要有 template <typename 类型名字> 在旧版本C++中使用 class 替代 typename,且必须使用<>
template<typename T> 
void swap(T & a, T & b)
{
    T temp = a;
    a = b;
    b = temp;
}

需要注意的是模版并不创建任何函数,只是告诉编译器如何定义函数,当需要交换int类型数据,编译器将按照模版替换生成int类型的函数。当需要double类型,编译器就会替换生成double类型的函数。 也就是说根据需求编译器会生成同一份模版的多个实例

// 尝试调用上述模版函数 分别用int和long
int a = 1;
int b = 2;
long c = 1L;
long d = 2L;
swap(a, b);
swap(c, d);

我们查看汇编代码,我们发现确实当我们调用不同类型的swap函数,C++编译器将根据模版函数生成对应于不同类型的多份函数。

	.globl	__Z4SwapIlEvRT_S1_              ; -- Begin function _Z4SwapIlEvRT_S1_  ; long 型交换函数
	.weak_definition	__Z4SwapIlEvRT_S1_
	;...  中间部分省去
	.cfi_endproc
                                        ; -- End 
	.globl	__Z4SwapIiEvRT_S1_              ; -- Begin function _Z4SwapIiEvRT_S1_ ; int 类型的交换函数
	.weak_definition	__Z4SwapIiEvRT_S1_
	;...  中间部分省去
	.cfi_endproc
                                        ; -- End function

通过上述例子我们发现函数模版并不能缩短可执行文件的大小的长度,最终仍是由多个类型的函数组成。就像手工定义一样。

而且最终代码不包含任何函数模版,只包含了为程序生成的实际函数

使用函数模版好处是使得生成多个函数定义更简单且更可靠

重载的模版

当你需要对多个不同类型使用同一个算法的时候,可使用模版。然而并非所有的类型都适用相同的算法,为了满足这种需求,可以向重载常规函数定义那样重载模版定义,和重载常规函数定义一样,被重载的函数模版特征标必须不同,看如下的函数原型

// 函数原型
template<typename T>
void swap(T & a, T & b);
template<typename T>
void swap(T a[], T b[], int length)
// 函数定义
template<typename T>
inline void swap(T & a, T & b)
{
    T temp = a;
    a = b;
    b = temp;
}
template<typename T>
void swap(T a[], T b[], int length)
{
    for (int i = 0; i < length; ++i) {
        swap<T>(a[i],b[i]);
    }
}

通过函数重载实现了swap交换元素和交换数组,特别要指出的是模版函数中可以带有非模版参数。

模版的局限性

假设有如下模版函数

template <typename T>
void f(T a,T b){
    // ...
    T c = a + b; // 如果是用户自定义类型,如何相加
    // ...
    if ( a > b ){ // 如果是用户自定义类型如何比较?
        //...
    }
    //...
}

面对这种问题C++提供两种解决方法,第一种运算符重载让其用于特定的结构和类,而另一种解决办法是为特定类型提供具体化的模版定义

显式具体化

看如下结构

struct job{
    char name[40];
    double salary;
    int floor;
};

如果用于执行之前的交换函数,没有问题,因为C++允许将一种结构赋给另一种结构。但是如果我们只是希望交换salary和floor两个成员而不交换name成员,则需要使用不同代码,而swap参数保持不变,因此无法通过模版重载来提供其他的代码。

但是可以提供一个具体化函数定义–称为显式具体化。其中包含所需的代码,编译器会优先使用与函数调用匹配的具体化函数定义。而不在寻找模版。

第三方具体化(ISO/ANSI C++标准)

  • 对于给定的函数名可以有非模版函数,模版函数,显式具体化函数和他们的重载版本
  • 显式具体化的函数原型和定义应以template<>打头,通过名称来指出类型
  • 具体化优于常规模版,而非模版函数优于具体化和常规模版

下面是三种函数的原型

// 非模版函数原型
void swap(job & a, job & b);
// 常规模版函数原型
template <typename T> void swap(T & a,T & b);
// 具体化模版函数原型
template <> void swap<job>(job & a,job & b);
// 具体化函数方括号也可以省去 写成
template <> void swap(job & a,job & b);

需要注意的是具体化函数是常规模版具体化,换句话说也就是必须先有常规模版,才能有具体化

实例化和具体化

我们必须要再次提及在代码中包含函数模版本身并不会生成函数定义,他只是一个用于生成函数定义的方案,编译器使用模版生成函数定义时,得到的是模版实例,模版并非函数定义。

看下述例子

// 模版原型 定义
template <typename T> void swap(T & a,T & b);
// 函数调用 (隐式实例化)
swap(1,2);

模版并不是函数定义,只有存在函数调用的时候会生成一个跟调用类型相同的函数实例,模版并非函数定义,但使用int类型的模版实例是函数定义,上述例子是隐式实例化,现在的C++编译器还支持显式实例化,意味者可以直接命令编译器直接创建特定类型的函数实例,无论是否有调用。

来看一个显式实例化的实例

template <typename T>
void swap(T & a, T & b){
    T t = a;
    a = b;
    b = t;
}
template void swap<int>(int & a,int & b);

上述函数即使是没有任何函数调用int 类型的swap函数,编译器都会生成int类型的交换函数。

显式实例化和显式具体化的区别

显式实例化的作用是命令编译器按照模版生成一个特定类型的函数定义。显式实例化使用模版来生成代码,没有定义

而显式具体化的作用是告诉编译器这个类型的变量不实用模版生成函数,而使用我里面的代码。需要定义。

// 显式具体化的原型
template <> void swap<job>(job & a,job & b);
// 显式实例化的原型
template void swap<int>(int & a,int & b);

不能在同一个文件中使用同一种类型的显式实话和显式具体化将会出错!,不能共存

隐式实例化,显式实例化,显式具体化都称为具体化(共同点:都会生成具体的执行代码)表示的都是具体类型的函数定义,而不是通用描述。

来看一段总结

// 常规模版
template<typename T> void swap(T & a, T & b){
    T tmp = a;
    a = b;
    b = tmp;
}
// 显式具体化
template<> void swap(job & a, job & b);
// 显示实例化
template void swap<int>(int & a,int & b);
int main(){
    long a = 1;
    long b = 2;
    // 隐式实例化
    swap(a,b);
    // 另一种显示实例化
    swap<long long>(a,b);
}

编译器选择使用哪个函数版本

对于函数重载,函数模版,函数模版重载,C++需要有一个定义良好的策略,来决定为函数调用使用哪一个函数定义。尤其是有多个参数的时候。这个过程称为重载解析。

  1. 创建候选函数列表,其中包括与被调用函数名称相同的函数和模版函数
  2. 使用候选函数列表创建可行参数列表,这些都是参数数目正确的函数,还有一个隐式转换序列,其中包括实参类型与相应形式参数完全匹配的情况,使用float函数调用可以转换为double来调用形参为double的函数,而模版可以生成一个float实例。
  3. 确定是否有最佳可行函数,有则使用,否则出错

看以下函数

// 函数原型
void may(int);
float may(float, float = 3);
void may(char);
char * may(const char *);
char may(const char &);
template<typename T> void may(const T &);
template<class T> void may(T *);
// 函数调用
may('B');

第四个和第七个一定不行,另外的五个如果是唯一的函数,也可以被调用,接下来编译器必须确定哪一个函数最佳,它查看为函数调用参数与可行的的候选函数的参数匹配需要进行的转换,通常最优到最差按照描述如下:

  1. 完全匹配,但常规函数优于模版函数
  2. 提升转换 char,short -> int 或者 float -> double.
  3. 标准转换 int -> char long -> double
  4. 用户定义的转换:类声明中的定义的转换

函数1优于函数2 char -> int 是提升转换而 char -> float 是标准转换,函数3,5,6优于1,2因为都是完全匹配,函数3,5优于6,因为6是模版函数,

什么是完全匹配

  1. 完全匹配和最佳匹配

进行完全匹配时,C++允许进行一种无关紧要的转换"无关紧要的转换"。

从实参到形参
TypeType &
Type &Type
Type []Type *
Type(argument-list)Type(*)(argument-list)
Typeconst Type
Typevolatile Type
Type *const Type *
Type *volative Type *

假设有以下函数代码

// 数据
struct blot{int a;char b[10];};
blot ink{25, "spots"};
// 函数原型
void recyle(blot);
void recyle(const blot);
void recyle(blot &);
void recyle(const blot &);
// 函数调用
recycle(ink);

如你所预期的一样如果有多个匹配的原型编译器将无法完成解析过程,如果没有最佳可行函数,则编译器将生成一条错误信息

但是有些时候即使两个函数完全匹配也可能完成解析,

  1. 指向非const数据的指针和引用优先与非const指针和引用参数匹配。也就是说如果只定义3,4那么是可以匹配的将会选择3

但是记住const和非const区别仅仅适用于指针和引用,如果只定义1和2仍然将出现二义性错误

  1. 一个完美匹配优于另外一个另外一种情况是其中一个时模版函数,另一个不是,在这种情况下非模版函数优于模版函数(包括显示具体化)。

  2. 如果两个完全匹配的函数都是模版函数,则更加具体的函数优先。意味者显示具体化要优先于模版隐式生成的具体化。

template <typename T> void recyle(T t); // 模版函数
template <> void recyle(blot & t); // 类型blot的显示具体化

// 数据
struct blot{int a;char b[10];};
blot ink{25, "spots"};
// 函数调用
recycle(ink);

则程序会调用显示具体化的而非利用模版创建一个。

  1. 术语最具体并不一定意味着显示具体化,而是指编译器推断使用那种类型时执行的转换最少。
// 函数原型
template <typename T> void recyle(T t);
template <typename T> void recyle(T * t);
// 函数调用
recycle(&ink);

由于 &ink 是 blot * 显然将 T * -> blot * 的转换要比从 T -> blot * 少,所以会选择第二个

或者换个角度来说第二个比第一个更具体,他直接指出类型时指针,而第一个没有。

创建自定义选择

在有些情况可以编写合适的函数调用来引导编译器作作出你希望的选择。

#include "bits/stdc++.h"
using namespace std;
template<typename T> T lesser(T c, T d) {return c < d ? c : d;} //# 1
int lesser(int a, int b) {
    a = a < 0 ? -a : a;
    b = b < 0 ? -b : b;
    return a < b ? a : b;
}
int main(){
    using namespace std;
    int m = 20;int n = -30;
    double x = 15.5;double y = 25.9;
    cout << lesser(m, n) << endl; // 2
    cout << lesser(x, y) << endl; // 1
    // 通过尖括号引导编译器选用模版函数
    cout << lesser<>(m, n) << endl; // 1
    // 通过尖括号引导编译器使用 int 类型的模版函数
    cout << lesser<int>(x, y) << endl; // 1
    return 0;
}

模版函数的发展

C++98的时候,一个问题是并非总能知道应在声明中使用哪种类型,请看下面代码

template<typename T1, typename T2>
void ft(T1 x, T2 y)
{
    // ...
    ?type? xpy = x + y;
    //...
}

x + y 是什么类型呢?可能是T1,可能是T2,或者是其他的类型。因此在C++98中无法声明xpy的类型

关键字 decltype (C++11)

C++11新增加的关键字 decltype 提供了解决方案,可这样使用关键字:

int x;
decltype(x) y;  // y 和 x 同一类型
// 提供给decltype的参数可以是表达式,上述可以改写如下
decltype(x + y) xyz = x + y;

具体实现分为三步

  1. 如果是没有括号括起的表达式,则类型与该标识符的类型相同,包括const等修饰符号
  2. 如果是函数调用则跟该函数返回值相同,并不会实际调用函数,而是查看原型
  3. 如果表达式是一个左值,则var为指向其类型的引用 (如果要进入第三部表达式必须要用括号括起)

另一种函数声明类型 C++后置返回类型

还有一个相关问题decltype也无法解决看如下代码

template<typename T1, typename T2>
?type? gt(T1 x, T2 y)
{
    //...
    return x + y;
}

我们无法在原型中使用decltype因为他还不存在必须在声明后使用decltype,C++新增了一种声明和定义函数的语法。

double h(int x,float y);
// 使用新语法
auto h(int x,float y) -> double;

将返回类型移动到了参数声明的后面,-> double 被称为后置返回类型,其中auto是一个占位符标识后置返回类型提供的类型。结合decltype就可以解决上述问题

template<typename T1, typename T2>
auto gt(T1 x, T2 y) -> de
{
    //...
    return x + y;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值