【C++】模板及模板的特化

2024-06-14 14:52
文章标签 模板 c++ 特化

本文主要是介绍【C++】模板及模板的特化,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

               

目录

一,模板

  1,函数模板

什么是函数模板

函数模板原理

函数模板的实例化

 推演(隐式)实例化

 显示实例化 

模板的参数的匹配原则

2,类模板

什么是类模板

类模板的实例化

二,模板的特化

1,类模板的特化

全特化

 偏特化

2,函数模板的特化——全特化

三,非类型模板参数


一,模板

  1,函数模板

        什么是函数模板

所谓函数模板,实际上是建立一个通用的函数,该函数类型和形参类型不具体指定,而是用一个表示任意类型的虚拟类型来代表(这里的任意类型可以任意选择,如 T)。

定义函数模板的一般形式:

template <typename T1, typename T2, ....., typename Tn>

返回值类型 函数名(参数列表) 

{

        // .....

}

       其中templateclass是关键字,typename 可以用class关键字代替,在这里typename 和class没区别,<>括号中的参数叫模板形参,模板形参和函数形参很相像,模板形参不能为空。一但声明了模板函数就可以用模板函数的形参名声明类中的成员变量和成员函数,即可以在该函数中使用内置类型的地方都可以使用模板形参名。模板形参需要调用该模板函数时提供的模板实参来初始化模板形参,一旦编译器确定了实际的模板实参类型就称他实例化了函数模板的一个实例。比如swap的模板函数形式为

下面来看一下,完成多个不同类型的两个数据的交换

针对具体类型(常规函数)

// 完成两个整形变量的交换
void Swap(int& left, int& right)
{int temp = left;left = right;right = temp;
}// 完成两个double型变量的交换
void Swap(double& left, double& right)
{double temp = left;left = right;right = temp;
}// 完成两个字符型变量的交换
void Swap(char& left, char& right)
{char temp = left;left = right;right = temp;
}int main()
{int a = 1, b = 2;double c = 1.1, d = 2.2;char ch1 = 'a', ch2 = 'b';Swap(a, b);Swap(c, d);Swap(ch1, ch2);return 0;
}

通过上面可以看出,若想要完成以上三种不同类型的交换须要写三个交换函数,这是针对每种类型分别写出具体的交换函数;但是以上的三种交换函数除了参数的类型不一样,其他都是一样的,显得代码既冗余又不够简练。来看下面使用函数模板

跟具体类型无关(函数模板)

//函数模板
//template<class T>
template<typename T>           // 声明一个类型模板参数 T>
void Swap(T& left, T& right)   // 使用模板参数T来声明函数参数left和right
{T temp = left;left = right;right = temp;
}int main()
{int a = 1, b = 2;double c = 1.1, d = 2.2;char ch1 = 'a', ch2 = 'b';Swap(a, b);Swap(c, d);Swap(ch1, ch2);return 0;
}

     在这个例子中,T 是一个类型模板参数,它告诉编译器我们希望这个函数能够处理多种类型。在函数模板的声明中,使用 typename 关键字(也可以使用 class 关键字,两者在函数模板中都是等价的)来声明类型模板参数。

    然后,使用类型模板参数 T 来声明函数的参数 left和 right,它们都是类型为 T 的引用。这意味着可以传递任何类型的变量给 swap 函数,只要这两个变量的类型相同。

函数模板原理

        当编译器遇到一个函数调用时,编译器会尝试根据传递给函数模板的实参类型来推导出模板参数的类型,一旦类型推导成功,编译器就会生成一个或多个具体的函数实例,这些实例的类型与推导出的模板参数类型相匹配。

注意:函数模板本身并不产生代码,它只是一个蓝图。只有在模板被实例化时,编译器才会生成具体的函数代码。

当实参a、b 是 int 时,编译器会把模板参数 T 推演成 int 类型,会实例化出一份具体类型的Swap 函数来调用;当实参a、b 是 double 时, 编译器会把模板参数 T 推演成 int 类型; char类型也一样。

注意:以上三个函数在实例化时虽然走的都是同一个函数模板,但是调用的不是同一个函数;只是用同一个函数模板实例化出了三份针对具体类型的函数

如下:

函数模板的实例化

         推演(隐式)实例化

隐式实例化:编译器在调用一个模板函数时,根据提供的参数类型自动推断出模板参数的类型,并生成相应的函数实例。这个过程是编译器自动完成的,不需要程序员显式指定模板参数的类型。

// 声明一个函数模板  
template <typename T>
T Add(T a, T b) 
{return a + b;
}int main()
{int a1 = 3, a2 = 5;double d1 = 3.5, d2 = 6.5;// 隐式实例化:编译器根据参数类型(int)推断出模板参数T为intAdd(a1, a2);    // 生成了 add<int>(int, int) 的实例 // 编译器根据参数类型(double)推断出模板参数T为doubleAdd(d1, d2);    // 生成了 add<double>(double, double) 的实例 Add(a1, d1);   // a1和d1是不同的类型,此时编译报错,编译器无法推演出具体的类型return 0;
}

上述第三个 Add(a1, d1); 该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型 通过实参a1将T推演为int通过实参d1将T推演为double类型,但模板参数列表中只有一个T, 编译器无法确定此处到底该将T确定为int 或者 double类型而报错

解决方法:1. 可以改成多参数的函数模板   如:template<typename T1, typename T2>

                  2. 手动强制类型转换    如:上面的  Add(a1, (int)d1); 或  Add((double)a1, d1);

                  3. 就是下面要说的 显式实例化   

 显示实例化 

    函数模板允许编写通用的函数,这些函数可以处理不同类型的数据。但是,在某些情况下,可能想要为特定的类型显式地实例化函数模板,以便在编译时生成具体的函数版本。

即:在函数名后的<>中指定模板参数的实际类型

// 声明一个函数模板  
template <typename T>
T Add(T a, T b)
{return a + b;
}int main()
{int a1 = 3, a2 = 5;double d1 = 3.5, d2 = 6.5;// Add(a1, d1);   // a1和d1是不同的类型,此时编译报错,编译器无法推演出具体的类型Add<int>(a1, d1);   // 显式实例化 可强制为 int 类型实例化,并将参数 d1 强制转换为 int 类型// 或Add<double>(a1, d1); // 显式实例化 可强制为 double 类型实例化,并将参数 a1 强制转换为 double 类型return 0;
}

上面的模板与函数调用 Add(a1, d1) 不匹配,因为该模板要求两个函数参数的类型相同。但通过使用 Add<int>(a1, d1),  可强制为 int 类型实例化,并将参数 d1 强制转换为 int 类型,这样就可以与函数 Add<int>(int, int) 的第二个参数匹配。

模板的参数的匹配原则

1. 同时存在性一个非模板函数可以和一个同名的函数模板同时存在。此外,这个函数模板还可以被实例化为这个非模板函数

2. 优先调用非模板函数非模板函数和同名函数模板在参数上相匹配时,编译器会优先调用非模板函数,而不是从模板产生出一个实例(即有现成的就吃现成的

3. 模板的更好匹配如果模板可以产生一个具有更好匹配的函数,那么编译器会选择模板而不是非模板函数(即有更合适的就吃更合适的,没有就将就吃)。

// 函数模板,可以处理任何类型的加法 
template <typename T>
T Add(T a, T b)
{return a + b;
}// 非模板函数,专门处理int类型的加法  
int Add(int a, int b) 
{return a + b;
}int main()
{int a1 = 3, a2 = 5;double d1 = 3.5, d2 = 6.5;// 调用非模板函数,因为参数是int类型,与非模板函数匹配 Add<int>(a1, a2); Add<double>(d1, d2);  // 调用模板函数,因为参数是double类型,与非模板函数不匹配Add<double>(a1, d1);  // 调用模板函数,模板函数会生成更匹配的,因为参数是double类型,与非模板函数不匹配 (a1会被强转成 double)return 0;
}

2,类模板

什么是类模板

        类模板是对一批成员数据类型不同的类的抽象。只需为这一批类所组成的整个类家族创建一个类模板,并给出一套程序代码,就可以用来生成多种具体的类(这类可以看作是类模板的实例),从而大大提高编程的效率。

类模板的基本结构如下:

//template <typename 参数名, typename ...> // 可以有多个类型参数,使用逗号分隔

template < class T> // 或使用typename代替class  
class 类名

{  
    // 类的定义,可以使用类型T作为成员变量、函数参数或返回类型  
};

其中,template 关键字用于声明一个模板,<class 参数名, ...> 部分定义了模板参数,这些参数在模板内部可以用作类型。class关键字是用来指示随后的标识符是一个类型名称的,但也可以使用 typename 关键字代替 class

栈模板类可以定义如下:

// 类模板
template<class T>
class Stack
{
public:Stack(int capacity = 4){cout << "Stack(int capacity = 4)" << endl;_a = new T[capacity];_top = 0;_capacity = capacity;}~Stack(){cout << "~Stack()" << endl;delete[] _a;_a = nullptr;_top = 0;_capacity = 0;}
private:T* _a;int  _top;int  _capacity;
};

在上面的例子中,定义了一个名为 Stack的类模板,它接受一个类型参数 T。这个 T 类型被用作栈中元素的类型。

类模板的实例化

int main()
{// 显示实例化Stack<int> st1;    // 实例化一份存储int 类型的栈Stack<double> st2;  // 实例化一份存储double 类型的栈return 0;
}

      在 main 函数中,分别实例化了两个 Stack 对象:一个用于存储整数(Stack<int>),另一个用于存储浮点数(Stack<double>)。这两个对象都使用了相同的 Stack 类模板,但是它们内部处理的数据类型是不同的。这就是类模板的强大之处:通过编写一次代码,就可-以创建出多种类型安全的栈类。

注意:1. 对于类模板,模板形参的类型必须在类名后的尖括号中明确指定。比如A<2> m;这种方法把模板形参设置为int是错误的,类模板形参不存在实参推演的问题。也就是说不能把整型值2推演为int 型传递给模板形参。要把类模板形参调置为int 型必须这样指定A<int> m

            2. 模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板

二,模板的特化

1,类模板的特化

类模板特化是针对特定类型的模板参数提供定制的类模板实现。它允许在某些情况下,使用与通用模板不同的实现方式。类模板特化分为全特化和偏特化(局部特化)两种

全特化

对模板参数列表中的所有模板类型都进行具体化。例如,如果有一个模板类Test<T1, T2>,我们可以为T1T2都是int的情况提供一个全特化的版本

template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
};// 全特化为 int和char
template<>
class Data<int, char>
{
public:Data() { cout << "Data<int, char>" << endl; }
private:int _d1;char _d2;
};// 全特化为 int int
template <>
class Data<int, int>
{
public:Data() { cout << "Data<int, int>" << endl; }
private:
};int main()
{Data<int, int> d1;Data<int, char> d2;return 0;
}

  写一个类的全特化,就相当于写一个新的类一样,你可以自己定义任何东西,不管是函数、数据成员、静态数据成员等等;根据自己的需求

 偏特化

偏特化就是如果这个模板有多个类型,那么只限定其中的一部分,即只对模板的部分类型进行明确指定

特化部分参数:将模板参数类表中的一部分参数特化

一个 Data的类模板,它接受两个类型参数 T1T2T2为 int 的情况提供了偏特化

//原模版
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
};// 特化部分参数
template <class T1>
class Data<T1, int>
{
public:Data() { cout << "Data<T1, int>" << endl; }
};int main()
{Data<char, int> d1;   // 走特化版本 Data<T1, int>Data<int, int> d2;Data<int, char> d3;   // 走原模板,第二个参数为char与第二个模板参数int不匹配return 0;
}

T2int时,编译器将使用偏特化的Data类模板,而不是原始的模板。如果T2不是int,则编译器将使用原始的模板

对参数类型进行一定限制。比如:限制是指针或者引用等

//对参数类型限制为指针
template <class T1, class T2>
class Data<T1*, T2*>
{
public:Data() { cout << "Data<T1*, T2*>" << endl; }
};//对参数类型限制为引用
template <class T1, class T2>
class Data<T1&, T2&>
{
public:Data() { cout << "Data<T1&, T2&>" << endl; }
};//对参数类型T1限制为指针,T2限制为引用
template <class T1, class T2>
class Data<T1*, T2&>
{
public:Data() { cout << "Data<T1*, T2&>" << endl; }
};int main()
{Data<char*, int*> d4;   // 走特化版本 Data<T1*, T2*>Data<int*, int*> d5;Data<int&, int*> d6;    // 走原模板 Data<int&, int&> d6;    // 走特化版本 Data<T1&, T2&>Data<int*, int&> d7;    // 走特化版本 Data<T1*, T2&>return 0;
}

注意:偏特化有一些限制。不能为一个非类型模板参数提供偏特化,也不能为一个函数模板提供偏特化同时,偏特化的结果仍然是一个模板,而不是一个具体的类

2,函数模板的特化——全特化

//函数模板
template<typename T1, typename T2>
void fun(T1 a, T2 b) 
{cout << "函数模板" << endl;
}//全特化
template<>
void fun<int, char >(int a, char b) 
{cout << "全特化" << endl;
}//函数不存在偏特化:下面的代码是错误的
/*
template<typename T2>
void fun<char, T2>(char a, T2 b) 
{cout << "偏特化" << endl;
}
*/

注意:对于函数模板,只有全特化,不能偏特化

三,非类型模板参数

模板参数分为类型形参非类型形参

类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称(上面已经介绍过)

非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用

// 定义一个模板类型的静态数组
template<class T, size_t N>  
class Array 
{  
public:    // ... 其他成员函数 ...private:T data[N];
};  int main() 
{  Array<int, 5> arr; // 创建一个大小为5的整型数组  // ...  
}

注意:

1. 非类型模板参数必须是常量表达式,也就是说它们必须在编译时就能确定其值

2. 非类型模板参数的类型通常是整数类型(如 intsize_t 等)、指针(如 指向函数的指针)或引用,但不能是类类型或其他复杂类型。

3. 非类型模板参数在模板实例化时会被替换为具体的值,因此它们会影响生成的代码的类型和布局


这篇关于【C++】模板及模板的特化的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1060716

相关文章

C++ 中的 if-constexpr语法和作用

《C++中的if-constexpr语法和作用》if-constexpr语法是C++17引入的新语法特性,也被称为常量if表达式或静态if(staticif),:本文主要介绍C++中的if-c... 目录1 if-constexpr 语法1.1 基本语法1.2 扩展说明1.2.1 条件表达式1.2.2 fa

C++中::SHCreateDirectoryEx函数使用方法

《C++中::SHCreateDirectoryEx函数使用方法》::SHCreateDirectoryEx用于创建多级目录,类似于mkdir-p命令,本文主要介绍了C++中::SHCreateDir... 目录1. 函数原型与依赖项2. 基本使用示例示例 1:创建单层目录示例 2:创建多级目录3. 关键注

C++从序列容器中删除元素的四种方法

《C++从序列容器中删除元素的四种方法》删除元素的方法在序列容器和关联容器之间是非常不同的,在序列容器中,vector和string是最常用的,但这里也会介绍deque和list以供全面了解,尽管在一... 目录一、简介二、移除给定位置的元素三、移除与某个值相等的元素3.1、序列容器vector、deque

C++常见容器获取头元素的方法大全

《C++常见容器获取头元素的方法大全》在C++编程中,容器是存储和管理数据集合的重要工具,不同的容器提供了不同的接口来访问和操作其中的元素,获取容器的头元素(即第一个元素)是常见的操作之一,本文将详细... 目录一、std::vector二、std::list三、std::deque四、std::forwa

C++字符串提取和分割的多种方法

《C++字符串提取和分割的多种方法》在C++编程中,字符串处理是一个常见的任务,尤其是在需要从字符串中提取特定数据时,本文将详细探讨如何使用C++标准库中的工具来提取和分割字符串,并分析不同方法的适用... 目录1. 字符串提取的基本方法1.1 使用 std::istringstream 和 >> 操作符示

C++原地删除有序数组重复项的N种方法

《C++原地删除有序数组重复项的N种方法》给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度,不要使用额外的数组空间,你必须在原地修改输入数组并在使用O(... 目录一、问题二、问题分析三、算法实现四、问题变体:最多保留两次五、分析和代码实现5.1、问题分析5.

C++ 各种map特点对比分析

《C++各种map特点对比分析》文章比较了C++中不同类型的map(如std::map,std::unordered_map,std::multimap,std::unordered_multima... 目录特点比较C++ 示例代码 ​​​​​​代码解释特点比较1. std::map底层实现:基于红黑

C++中函数模板与类模板的简单使用及区别介绍

《C++中函数模板与类模板的简单使用及区别介绍》这篇文章介绍了C++中的模板机制,包括函数模板和类模板的概念、语法和实际应用,函数模板通过类型参数实现泛型操作,而类模板允许创建可处理多种数据类型的类,... 目录一、函数模板定义语法真实示例二、类模板三、关键区别四、注意事项 ‌在C++中,模板是实现泛型编程

利用Python和C++解析gltf文件的示例详解

《利用Python和C++解析gltf文件的示例详解》gltf,全称是GLTransmissionFormat,是一种开放的3D文件格式,Python和C++是两个非常强大的工具,下面我们就来看看如何... 目录什么是gltf文件选择语言的原因安装必要的库解析gltf文件的步骤1. 读取gltf文件2. 提

C++快速排序超详细讲解

《C++快速排序超详细讲解》快速排序是一种高效的排序算法,通过分治法将数组划分为两部分,递归排序,直到整个数组有序,通过代码解析和示例,详细解释了快速排序的工作原理和实现过程,需要的朋友可以参考下... 目录一、快速排序原理二、快速排序标准代码三、代码解析四、使用while循环的快速排序1.代码代码1.由快