入门C++模板编程(一) 函数模板

我是FFZero,一个10年开发经验的cpper。

今天开始我来和大家一起入门C+模板编程。

模板的引入

C++作为一门强类型语言,执行相同操作时,需要针对每种类型单独定义函数。
举一个经典的求和的例子,对于求和操作,针对int类型参数和double类型参数实现除了参数类型外完全相同的代码。

int Add(int x,int y) {
    return x+y;
}

double Add(double x,double y) {
    return x+y;
}

于是,为了简化代码,模板编程,也叫泛型编程,应运而生。

C++模板编程分为函数模板和类模板,本文先学习函数模板。

函数模板是什么?

使用任意类型(泛型)描述函数,在编译时由编译器根据实参类型自动推导,生成该类型的函数定义,这个过程又被称为实例化

以上面求和为例,模板函数的代码简化成了:

template<typename T>   //typename关键字可以替换为class关键字
T Add(T x, T y) {
    return x + y;
}

int x = 0; y =1;
Add(x, y);      //不显示指定类型,编译器自动推导生成 int Add(int ,int)函数
Add<int>(x,y);  //显示指定类型生成 int Add(int ,int)函数

函数模板怎么用?

函数模板很好用,但是也不是万能的,下面的使用规则比较重要。

  1. 没有显示指定函数模板的数据类型,不会发生隐式转换

    以上面模板函数Add的调用进行举例。

//  显示指定函数模板的数据类型
int x = 0, char y = 1;

Add(x, y);       //模板无法选取类型,编译错误
Add<int>(x,y);   //生成 int Add(int ,int)函数,调用时发生隐式转换
  1. 函数模板可以使类的成员函数,但是不能是虚函数和析构函数

  2. 函数模板支持重载,重载函数可以有非泛型参数(选取规则按最优匹配来,可以通过编译器来看生成的函数)

// 模板函数重载
template<class T1, class T2>
void func(T1 arg1, T2 arg2) {
    cout << "模板1: " << arg1 << " " << arg2 << endl;
}

template<class T>
void func(T arg1, T arg2)  {
    cout << "模板2: "  << arg1 << " " << arg2 << endl;
}

template<class T,class T2>
void func(T arg1, T arg2, int arg3) {
    cout << "模板3: "  << arg1 << " " << arg2 << "" << arg3 <<  endl;
}

int main() {
    int x = 0;
    double y = 1;
    func(x, y);     // 使用模板1 
    func(x, x);   // 使用模板2
    func<double, int>(x, x, 2);  //使用模板3,这里不显示指定类型无法匹配 
    func(x,x,2); //编译报错,“func”: 未找到匹配的重载函数。 T2类型没有提供,模板3期望两个类型参数,而调用只提供了一个
}

不指定时推导为func(int,int,int)
思考:
下面的调用可以成功吗?

template<class T,class T2>
void func(T arg1, T2 arg2, int arg3) 

func(x,x,2);   //可以,T->int, T2->int
func(x,y,2);   //可以,T->int, T2->double
template<class T,class T2>
void func(T arg1, T arg2, T2 arg3) 

func(x,x,2);    //可以,T->int, T2->int
func(x,y,2);    //失败,参数类型为(int, double, int), T无法匹配
func<int, int>(x, y, 2); //可以, T指定为int,调用时y进行隐式转换

模板特化

模板特化是一个非常重要和实用的使用方法。

如果某个特殊类型与通用类型的实现不同,可以对函数模板进行特化,也可以叫函数模板的具体化。

template <typename T, typename U>
void func(T arg1, U arg2) {
    // 泛型实现
}

// 完全特化版本
template <>
void func<int, double>(int arg1, double arg2) {
    // 特化为 int 和 double 的实现
}

这样一来,同一个函数就可能有三种版本:普通函数、常规模板函数、模板函数的特化版本
那么编译器怎么选择调用函数的版本呢?

  1. 普通函数优于模板特化版本,模板特化版本优于常规模板版本
  2. 如果想使用函数模板,可以用空模板参数
  3. 如果函数模板能产生更好的匹配,将优于非模板函数
//(1) 普通函数
int Add(int x, int y) {
    cout << "Add(int, int)" << endl;;
    return x + y;
}

// (2) 普通模板
template<typename T>   //typename关键字可以替换为class关键字
T Add(T x, T y) {
    cout << "template<T>Add(T, T)" << endl;
    return x + y;
}

// (3) 特化
template<>
int Add(int x, int y) {
    cout << "template<int>Add(int, int)" << endl;
    return x + y;
}

int main() {
    Add(1,2);   // 都可以匹配, 优先级: (1) > (3) > (2)
    Add<>(1,2);  // (1)不可以匹配, 优先级:(2) > (3)
    Add(1.0, 2);  // 只有(1)可以匹配
    Add(1.0, 2.0);    //double类型更合适,所以(2) -> (1)
}

模板的定义和实现怎么分文件编写?

  1. 函数模板只是函数的描述,没有实体,创建函数模板的代码放在头文件中。

  2. 函数模板的特化有实体,编译的原理和普通函数一样,所以声明放在头文件中,定义放在源文件中,如果都在头文件中,多次include会产生函数重复定义错误。

思考:为什么模板定义在头文件中,模板实例化时不会产生重复定义错误?
答: 模板实例化的独特性质:C++ 编译器负责确保每个模板实例只会被实例化一次,即使模板的定义在多个不同的源文件中被包含和使用。编译器在实例化模板时会进行必要的检查,以避免重复定义。

  1. 链接器的支持:如果模板在多个源文件中被实例化,链接器将识别出所有实例化的相同实体,并确保最终的可执行文件中只包含一个定义。
  2. 内联和模板:大多数编译器将模板函数视为内联函数处理。内联函数允许在多个源文件中重复定义,只要所有定义都是相同的。因此,模板函数不会导致重复定义错误。

下面拿三种函数类型举例:

public.h:

#pragma once

void Swap(int a, int b);   //普通函数声明
 
template<typename T>     //模板函数声明+定义
void Swap(T a, T b) {
    cout << "函数模板" << endl;
}

template<> 
void Swap<int>(int a, int b);   //特化模板函数声明

public.cpp:

#include "public.h"

void Swap(int a, int b) {
    cout << "普通函数" << endl;
}

template<>
void Swap<int>(int a, int b) {
    cout << "函数模板的特化版本" << endl;
}

进阶技巧

模板函数返回值的类型推导。

模板函数如果存在依赖不同类型的计算结果的返回值,无法在声明时确定类型,可以使用decltype关键字进行自动推导。前置一个auto,最后“ -> decltype”才是真正的返回值类型。

template<typename T1, typename T2>
auto func(T1 a, T2 b) -> decltype(a + b)
{   
    decltype(a + b) tmp = a + b;
    return tmp;
}

decltype可以根据表达式的结果推导出类型。规则也很多,下次可以单独写个文章继续学习,这里就不拓展讨论了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值