【c++】模板详解(2)

简介: 本文深入探讨了C++模板的高级特性,包括非类型模板参数、模板特化和模板分离编译。通过具体代码示例,详细讲解了非类型参数的应用场景及其限制,函数模板和类模板的特化方式,以及分离编译时可能出现的链接错误及解决方案。最后总结了模板的优点如提高代码复用性和类型安全,以及缺点如增加编译时间和代码复杂度。通过本文的学习,读者可以进一步加深对C++模板的理解并灵活应用于实际编程中。

前言

       之前我们深入探讨了模板的概念、重要性及其在C++编程中的应用:


https://developer.aliyun.com/article/1640212?spm=a2c6h.24874632.expert-profile.28.27e929be7RurPk


通过模板,我们实现了代码的复用,并且初步理解了泛型编程。本篇文章,我们将继续学习模板的相关知识,进一步加深对模板的理解。


一、非类型模板参数

       之前我们在定义模板时,模板参数都是类型,而实际上模板参数可分为类型参数和非类型参数,非类形参数在模板实例化时需要被具体的值所替代。通常情况下,非类型参数都是常量表达式。所以在模板当中,我们就可以将非类型参数当作一个常量来使用。


代码示例:

#include <iostream>
using namespace std;
 
template<class T, size_t N = 10>
class A
{
public:
    size_t size()
    {
        return N;
    }
 
    //...
private:
    T _arr[N] = { 0 };//创建一个固定大小的数组
};
 
int main()
{
    A<int> arr;
    cout << arr.size() << endl;
    return 0;
}



这里需要注意:


1. 与类型参数相同,非类型参数可以在参数列表中指定默认值,也可以不指定。但是这个值必须在编译阶段就能确认其结果。例如:

int main()
{
    int a = 5;
    A<int, a> arr1;//编译报错
 
    A<int, 5> arr2;//正确
    return 0;
}

2. 在c++20之前,非类型参数只能是整形、枚举类型或指针类型的值。c++20之后,才可以使用其他类型的值作为参数。


二、模板的特化

1. 概念

       模板的特化指的是在模板的基础上,我们针对某些特定的类型或值,提供一种特殊的实现方式。当模板被实例化为这种特定类型时,就会根据新的实现方式进行推演,就像是“私人定制”。


2. 场景举例

       例如我们现在要实现一个函数模板,用于通用类型的大小比较:

template<class T>
bool less(T v1, T v2)
{
    return v1 < v2;
}

这种实现方式,对于内置类型和具有比较运算符重载的类类形而言,都可以达到预期效果。但是如果我们传入指针类型呢?


       如果传入指针类型,那么我们的原本期望应该是将两指针指向的数据进行比较,但是该模板推演的结果会将两个指针进行大小比较,是没有意义的。此时我们就可以实现一个针对指针类型的特化版本(只能针对特定指针进行特化)。


3. 函数模板的特化

       接下来,我们就针对刚才的例子,写一个特化版本的函数模板:

#include <iostream>
using namespace std;
 
//基础版本
template<class T>
bool Less(T v1, T v2)
{
    return v1 < v2;
}
 
//针对指针类型的特化版本
//注意:只能针对特定的指针类型进行特化,这里使用int*
template<>
bool Less<int*>(int* v1, int* v2)
{
    return *v1 < *v2;
}
 
int main()
{
    int a = 0, b = 1;
    int* pa = &a, * pb = &b;
 
    cout << Less(pa, pb) << endl;
    return 0;
}


运行结果:



这里要注意以下几点:


1. 函数模板特化时,必须要先有一个基础的函数模板存在。


2. 函数名之前的“template<>”不能省略。


3. 特化版本的函数参数必须和基础版本一一对应(例如该示例当中,基础版本v1、v2的类型都是T,针对int*类型的特化版本中v1、v2的类型都必须是int*),否则会出现编译错误。


       虽然这里的实现方式看起来比较高大上,但了解模板参数匹配原则的小伙伴们都应该知道,我们可以直接实现一个同名函数来解决问题:

bool Less(int* v1, int* v2)
{
    return *v1 < *v2;
}

所以说一般情况下,在函数模板中,我们需要针对特定类型执行特殊操作时,为了保证代码的可读性和简洁性,直接实现一个同名函数即可,不建议特化。相比函数模板的特化,类模板的特化更为常用。


4. 类模板的特化

       类模板的特化可以分为全特化和偏特化。


全特化

       全特化指的是将模板参数列表中所有的参数都确定下来。 例如:

//基础版本
template<class T1,class T2>
class A
{
public:
    //...
private:
    T1 _a;
    T2 _b;
};
 
//全特化版本
template<>
class A<int,char>//特化为int、char类型
{
public:
    //...
private:
    int _a;
    char _b;
};


注意:只有当传入的所有模板参数都符合全特化场景时,类模板才会根据全特化版本进行实例化


A<int, int> a;//调用基础版本

A<int, char> a;//调用全特化

偏特化

        偏特化有两种表现形式:


1. 部分特化

       顾名思义,部分特化就是将模板参数中的一部分参数进行特化。例如:

//基础版本
template<class T1,class T2>
class A
{
public:
    //...
private:
    T1 _a;
    T2 _b;
};
 
//部分特化
template<class T1>//没有特化的部分
class A<T1, int>//将T2特化为int类型
{
public:
    //...
private:
    T1 _a;
    int _b;
};


注意:只要部分特化中特化的参数与传入的模板参数完全匹配,就根据部分特化版本进行实例化

A<int, double>;//调用基础版本
A<int, int> a;//调用部分特化

2.  对参数的限制

       除了部分特化之外,对参数的某些条件限制也可以称为偏特化。例如:

//基础版本
template<class T1, class T2>
class A
{
public:
    //...
private:
    T1 _a;
    T2 _b;
};
 
//偏特化为指针类型
template<class T1, class T2>
class A<T1*, T2*>
{
public:
    //...
private:
    T1 _a;
    T2 _b;
};
 
//偏特化为引用类型
template<class T1, class T2>
class A<T1&, T2&>
{
public:
    A(const T1& a,const T2& b)
        :_a(a)
        ,_b(b)
    {}
    //...
private:
    const T1& _a;
    const T2& _b;
};


A<int*, int*> a1; //调用指针偏特化
A<int&, int&> a2(1, 2); //调用引用偏特化

另外需要注意:当传入的模板参数同时满足全特化和偏特化的条件时,优先选择全特化。


三、模板的分离编译

       首先讲讲什么是分离编译:


一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。


之前我们在 “模板详解(1)” 中提到: 声明和定义不应分离到两个文件,否则会出现链接错误。今天我们来探讨一下出现链接错误的原因:


       假设现在有一个函数模板,它的声明和定义分别在头文件和源文件中:

//a.h
template<class T>
T Add(const T& left, const T& right);
 
//a.cpp
template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}
 
//main.cpp
#include"a.h"
int main()
{
    Add(1, 2);
    Add(1.0, 2.0);
    return 0;
}


那么在程序执行之前就会出现这样的情况:



当我们传参之后,源文件中的函数模板并不知道要实例化为什么类型,所以会发生链接错误。


      解决方案:


1. 将声明和定义放在同一头文件当中(推荐)


2. 在定义位置进行显式实例化的声明(不推荐,很不实用)


四、模板优缺点总结

       最后,我们总结一下c++中模板的优点和缺点:


优点

1. 提高代码复用性:可以在不同的数据类型上生成相同的代码,减少重复代码的编写,提高了代码的可维护性和可读性。

2. 类型安全:模板在编译时会进行类型检查,确保类型的正确。

3. 灵活性:模板可以适应不同的数据类型和数据结构,提供灵活的编程方式。通过特化操作,模板可以针对不同的需求生成特定的代码,满足不同的应用场景。

4. 可扩展性:模板提供了一种扩展C++语言的机制,可以通过在模板中添加特定的功能来扩展语言的能力(如STL),满足了复杂的应用需求。


缺点

1. 编译时间开销:由于模板在编译时需要实例化多个版本,增加编译时间(特别在大型项目中)。

2. 代码膨胀:模板在编译时生成大量代码,这可能会增加可执行文件的体积,占用更多的内存资源。

3. 复杂性:模板代码可能比直接编写的代码更复杂和难以理解。特别是当模板涉及多个参数和复杂的特化时,代码的可读性会降低。

4. 调试困难:由于模板代码在编译时生成,调试时不容易找到到原始模板代码的位置。此外,错误信息凌乱,不易定位错误。


总结

       今天, 我们学习了非类型模板参数、模板特化以及模板分离编译的相关知识,进一步加深了对模板的理解。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

相关文章
|
1月前
|
编译器 C++
模板(C++)
本内容主要讲解了C++中的函数模板与类模板。函数模板是一个与类型无关的函数家族,使用时根据实参类型生成特定版本,其定义可用`typename`或`class`作为关键字。函数模板实例化分为隐式和显式,前者由编译器推导类型,后者手动指定类型。同时,非模板函数优先于同名模板函数调用,且模板函数不支持自动类型转换。类模板则通过在类名后加`&lt;&gt;`指定类型实例化,生成具体类。最后,语录鼓励大家继续努力,技术不断进步!
|
6月前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
169 10
|
2月前
|
编译器 C++
㉿㉿㉿c++模板的初阶(通俗易懂简化版)㉿㉿㉿
㉿㉿㉿c++模板的初阶(通俗易懂简化版)㉿㉿㉿
|
8月前
|
编译器 C++
【C++】——初识模板
【C++】——初识模板
【C++】——初识模板
|
2月前
|
存储 安全 算法
深入理解C++模板编程:从基础到进阶
在C++编程中,模板是实现泛型编程的关键工具。模板使得代码能够适用于不同的数据类型,极大地提升了代码复用性、灵活性和可维护性。本文将深入探讨模板编程的基础知识,包括函数模板和类模板的定义、使用、以及它们的实例化和匹配规则。
|
5月前
|
安全 编译器 C++
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
183 4
|
5月前
|
算法 编译器 C++
【C++】模板详细讲解(含反向迭代器)
C++模板是泛型编程的核心,允许编写与类型无关的代码,提高代码复用性和灵活性。模板分为函数模板和类模板,支持隐式和显式实例化,以及特化(全特化和偏特化)。C++标准库广泛使用模板,如容器、迭代器、算法和函数对象等,以支持高效、灵活的编程。反向迭代器通过对正向迭代器的封装,实现了逆序遍历的功能。
73 3
|
5月前
|
编译器 C++
【c++】模板详解(1)
本文介绍了C++中的模板概念,包括函数模板和类模板,强调了模板作为泛型编程基础的重要性。函数模板允许创建类型无关的函数,类模板则能根据不同的类型生成不同的类。文章通过具体示例详细解释了模板的定义、实例化及匹配原则,帮助读者理解模板机制,为学习STL打下基础。
69 0
|
6月前
|
编译器 程序员 C++
【C++打怪之路Lv7】-- 模板初阶
【C++打怪之路Lv7】-- 模板初阶
54 1
|
6月前
|
存储 编译器 C++
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
87 9

热门文章

最新文章

相关实验场景

更多
下一篇
oss创建bucket