C/C++语言基础--C++模板与元编程系列一(泛型、模板、函数模板、全特化函数模板………)

时间:2024-10-25 12:15:18

本专栏目的

  • 更新C/C++的基础语法,包括C++的一些新特性

前言

  • 模板与元编程是C++的重要特点,也是难点,本人预计将会更新10期左右进行讲解,这一期是入门。
  • 考试周,会更新的比较慢​????​​????​​????​​????​​????​​????​;
  • C语言后面也会继续更新知识点,如内联汇编;
  • 欢迎收藏 + 关注,本人将会持续更新。

文章目录

  • 模板与泛型
    • 基本概念
      • 泛型
      • 模板
      • 为什么要使用模板?
    • 函数模板
      • 函数模板定义格式
      • 函数模板调用
      • 模板实例化探究
        • 隐式实例化
        • 显示实例化(特化)
        • 删除指定的特化版本

模板与泛型

基本概念

泛型

????概念:它是一种泛化的编程方式,其实现原理为程序员编写一个函数/类的代码示例,让编译器去填补出不同的函数实现。允许您延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候。换句话说,泛型允许您编写一个可以与任何数据类型一起工作的类或方法。

总结:

  • 泛型原理:程序员编写一个函数/类的框架,编译器去填补里面的内容;
  • 延迟:先编写框架,在程序使用他的时候在进行实例化。

模板

模板是泛型(泛型generic type——通用类型之意)编程的基础,是创建泛型类或函数的蓝图或公式。

???? 模板概念

模板原本是工程上的概念,我们用浇注这个工序更好理解。首先你得有个模具,里面是空心的,空心的形状正好是最终工件的形状。然后我们把铁水浇注进去,等冷却后,打开模具,取出里面的工件。

  • 提取内容:
    • 先有摸具,但是里面是空心的
    • 浇注:铁水浇注后,取出里面工具

???? 类比C++

  1. C++的模板不是实际代码,他只是一个框架,需要用模板来生成代码(这个过程叫做实例化);
  2. 用模板实例化的代码,取决于模板参数,这也是为什么很多时候我们写模板的时候没报错,但是一旦编译就错了。

模板是一种对类型进行参数化的工具,可以是如下形式:

  • 函数模板:
  • 类模板:
  • 别名模板(C++11):
  • 变量模板(C++14):
  • 约束与概念(C++20):

为什么要使用模板?

假如设计一个两个参数的函数,用来求两个对象的和,在实践中我们可能需要定义n多个函数

int add(int a,int b){return a+b;}
char add(char a,char b){return a+b;}
float add(float a,float b){return a+b;}
...

以上例子

  • 功能都相同,唯一区别就是类型不同,但是这样却要写大量重复代码。

用模板书写如下

// 定义
template<typename T>
T add(T a, T b) {
    return a + b;
}

// 调用
add<int>(5, 6);   // int类型相加
add<char>('a', 'b');   // char类型
add<float>(1.0, 2.0);   // 浮点类型

// 当然也可以自动推导
add(2, 3);  // 自动推导int,但是本人不建议

这样写就简单很多了。

PS:编译器根据实参生成特定的函数的过程叫做模板实例化

函数模板

根据上面模板的定义,再联系到这里,可以把函数模板看作也是有两步操作

  1. 先搭框架,把函数的框架搭好
  2. 调用的时候,填内容

那填什么内容呢??

  • 细想,能填的也只有变量类型了,事实也确实如此,填写变量类型
  • 那这样我们就需要一个东西在搭建框架的时候把这个变量位置给占了

函数模板

  • 上面搭建的函数通用框架,就叫做函数模板。

函数模板定义格式

template<typename Type,...>	//注意后面不能加分号,... 代表可以定义不止一个参数
Type funName(Type val)
{
    //Code
}
  • **template:**是声明模板的关键字,告诉编译器开始泛型编程
  • <typename type>:尖括号中的typename是定义模板形参的关键字也就是这个模板形参是类型typename还可以用class关键字来代替,但是推荐用typename,class是很久以前的标准了。

案例:

编写一个对任意类型的两个对象相加的函数模板。

template<typename T>
T add(T a,T b)
{
    return a+b;
}

int main()
{
	cout << add<int>(2, 4) << endl;
	return 0;
}

输出结果:6

调用add的,编译器主要步骤

  • 之前是否有生成过add模板实例,如果没有,则实例化模板,生成一个int类型的函数,如果有则调用

???? 总结

模板函数调用也是调用函数,不是调用模板本身,模板只是框架。

函数模板调用

函数模板的调用,实际上也就是说如果将模板实例化为函数,从而调用对于函数,对于函数模板,有两种调用方式,分为显示和隐示(自动推导):

  • 显示类型调用:
    • add(2,4),在函数名后面写上**<实参类型>**,这里表示参数是int类型。
    • add<>(2, 4),<>里也可以不写类型,加上<>表示调用的是函数模板。
  • **自动类型推导:**根据参数的类型进行推导, 但是两个参数的类型必须一致,否则会报错
    • add(‘a’,‘c’)

那么需要传两个类型不一样的参数要怎么做呢?

  • 写两个模板参数类型即可,然后返回类型使用auto自动推导。
template <typename T,typename U>
auto add(T a,U b)
{
    return a + b;
}

???? 如:

#include <iostream>

template<typename T, typename U>
auto add(T a, U b)
{
	return a + b;
}

int main()
{
	std::cout << add<int, float>(30, 40.1) << std::endl;
	
	return 0;
}

输出:

70.1

模板实例化探究

隐式实例化

隐式就是是我们定义模板的时候,不指定任何类型,只用typename 关键字声明变量,具体类型取决于调用模板的时候传递变量类型或者是编译器自动推导,本文上面的模板都可以称为隐式实例化。

???? 上面提到,在实例化函数模板的时候,编译器回先检查是否创建过这个实例化的函数,那我们可以怎么检验呢?

  • 利用static修饰的静态变量
template<typename T>
int print()
{
	static int id = 0;
	return id++;
}

int main()
{
	cout << print<int>() << endl;		//输出:0
	cout << print<int>() << endl;		//输出:1
	cout << print<float>() << endl;		//输出:0
	cout << print<double>() << endl;	//输出:0
	return 0;
}

结果:

在这里插入图片描述

???? 很显然,print<int>两次调用的时候第二次输出为1,这样很好的说明了调用的是一个函数。

显示实例化(特化)

???? 让我们来看以下这个场景,实现字符串相加

#include <iostream>

template<typename T>
T add(T a, T b)
{
	return a + b;
}

int main()
{
	
	char a[10] = "Hello";
	char b[10] = "World";
	std::cout << add<char*>(a, b) << std::endl;

	return 0;
}

???? 结果预料之中,报错

在这里插入图片描述

???? 因为字符串是用字符数组来进行存储,传递a,b两个参数其实就是传递数组指针,指针可以进行加减运算,但是指针之间不能相加,想要实现字符串相加,需要调用strcat函数,注意:C++的std::string类实现字符串相加他是通过运算符重载来实现,也不是直接用+进行相加。


想要实现字符串的拼接,可以调用strcat函数来实现字符串的拼接,但是strcat函数是用来实现字符串的拼接的,其他,如整数就不适合了, 那这样这个模板不是万能的??

  • 当然不是万能的,但是C++为了兼顾这种情况,提出了特化的概念,可以在定义模板的时候,自己指定参数,结合C++的重载,就可以解决以上bug了,如下代码:

  • #include <iostream>
    #include <string.h>
    
    template<typename T1, typename T2>
    auto add(T1 a, T2 b)
    {
    	return a + b;
    }
    
    template<>
    auto add<char*, const char*>(char* a, const char* b)
    {
    	return strcat(a, b);
    }
    
    
    int main()
    {
    	char a[20] = { "Hello" };
    	std::cout << add(a, "World") << std::endl;
    
    	return 0;
    }
    

从这段代码可以看出,特化就是在原来的模板上,创建新的模板函数手动指定类型
结果:
在这里插入图片描述


特化有以下特点

  • 模板参数必须需要两个或者两个及以上
  • 特化结构和原来模板一直,不可修改,比如:返回值给是一致,只是修改参数类型

????‍♂ 可以探索:字符串为什么比其他类型更加接近内存??

注意:我们这里的特化解决方法和原来那个模板不一样

// 原来
template<typename T>
T add(T a, T b)
{
	return a + b;
}

原因

  • strcat第一个参数是char*类型,第二个参数是const char*类型,所以必须使用两个不类型的模板解决,所以这里解决特化是创建了一个新的类。
删除指定的特化版本

???? 如果我们不想让调用者传递const char*类型的参数,则可以删除const char*版本的特化。

template<>
const char* add<const char*>(const char*, const char*) = delete;

删除之后,cout << add("hello ", "world") << endl;这行代码将直接编译报错:error C2280: “const char *add<const char*>(const char *,const char *)”: 尝试引用已删除的函数


2️⃣ 模板特化可以分为两类,全特化和偏特化

  • 全特化:将模板中的所有参数确定化。

  • 偏特化:针对模板参数进一步进行条件限制的特化。

**注意:**而函数模板不支持偏特化,类模板才支持偏特化。

template<typename T1, typename T2>  // 模板
auto add(T1 a, T2 b)
{
    return a + b;
}

template<>
auto add<int, double>(int a, int b) // 全特化
{
    return a + b;
}

注意: 函数模板只有全特化,没有偏特化,偏特化在类模板中会详细讲解