【C++要笑着学】STL Array | 非类型模板参数 | 模板的特化 | 全特化与半特化 | 模板的优缺点

简介: 我们之前讲过C++的模板,考虑到当时还没有将 STL,所以并没有一次性讲完,我们把剩余的部分放到了讲完部分 STL 容器的后面去讲,这样比较方去讲解。比如我们本章我们会通过 STL 的 array 去讲解非类型模板参数。本章还会重点讲解模板的特化,最后简单的探讨一下C++引入模板的优缺点。

💭 写在前面


我们之前讲过C++的模板,考虑到当时还没有将 STL,所以并没有一次性讲完,我们把剩余的部分放到了讲完部分 STL 容器的后面去讲,这样比较方去讲解。比如我们本章我们会通过 STL 的 array 去讲解非类型模板参数。本章还会重点讲解模板的特化,最后简单的探讨一下C++引入模板的优缺点。



Ⅰ. 非类型模板参数(Nontype Template Parameters)


0x00 引入:什么是非类型模板参数?

" 对于函数模板和类模板,模板参数并不局限于类型,普通值也可以作为模板参数 "

5200f1f21099e976c17c2ccde7a1bf40_685a647b54434524bacdd51df1fd098d.png

 STL 的 array 就有一个非类型模板参数。


注意看,我们普通定义的 T 是类型,而 N 这里并不是类型,而是一个常量!


类型模板参数定义的是虚拟类型,注重的是你要传什么,而 非类型模板参数定义的是常量。

"非类型模板参数"
                     👇
template<class T, size_t N> class array;
             👆    
        "类型模板参数"

0x01 非类型模板参数的使用场景

💭 举例:假设我们要定义一个静态栈:

#define N 100
template<class T> 
class Stack {
    private:
        int _arr[N];
        int _top;
};

❓ 思考:我现在想定义两个容量不一样的栈,一个容量是100 另一个是 500,能做到吗?

df5729af1c4266b71a83766f852fbd9b_45da3aaf58244a33b3777f8b5e06bb21.png

这就像 typedef 做不到一个存 int 一个存 double,而使用模板可以做到 st1 存 int,st2 存 double。


这里你的 #define 无论是改 100 还是改 500 都没办法解决这里的问题,


对应的,这里使用非类型模板参数就可以做到 s1 存 100,s2 存 500。


💡 解决方案:定义一个常量 N

// #define N 100
template<class T, size_t N>
class Stack {
    private:
        int _arr[N];
        int _top;
};
int main(void) 
{
    Stack<int, 100> st1;     // 我期望它的大小是100
    Stack<double, 500> st2;  // 我期望它的大小是500
    return 0;
}

这里我们在模板这定义一个常量 N,派遣它去做数组的大小。


于是我们就可以在实例化 Stack 的时候指定其实例化对象的大小了,分别传 100 和 500。


这比宏更爽!它能传一个常量过去,定义不同的对象可以传不同的常量过去。


0x02 一些值得注意的点

📌 注意事项 ①:非类型模板参数是是常量,是不能修改的。

template<class T, size_t N> 
class Stack {
    public:
        void f() {    // 修改常量试试看
            N = 10; 
        }
    private:
        int _arr[N];
        int _top;
};
int main(void) 
{
    Stack<int, 100> st1;
    st1.f();
    return 0;
}


🚩 运行结果:(报错)

test1711.cpp:10:15: error: lvalue required as left operand of assignment
             N = 10;
             ~~^~~~

📌 注意事项②:有些类型是不能作为非类型模板参数的,比如浮点数、类对象以及字符串。


非类型模板参数基本上都是整型,实际上也只有整型是有意义和价值的(可以这么理解)。

2a681ee09cb77d530eece3696b3bc5c8_204b066d0221412ab8628af4ea93396e.png

(char 也算整型,只不过是一个字节的整型,你不能因为它一个字节就歧视它)


📌 注意事项③:非类型的模板参数必须在编译期就能确认结果。


0x03 顺便介绍一下 STL 的 array

" 我们对 STL 的 array 安排的场次如此潦草,在这里穿插下随便讲讲好啦 "


🔍 官方文档:array - C++ Reference


现在学了非类型模板参数了,我们现在再来回头看 array:

9233f5d2077eed766c5f1a6265e26605_7d19f7e6923847499123226406d851ac.png

array 是 C++11 新增的,它有什么独特的地方吗?


很可惜,基本没有,并且 vector 可以完全碾压 array……


而且就算说它有,那也不是优势反而是劣势,这就是为什么我们没有这么积极的讲解 array。

#include <iostream>
#include <array>
#include <vector>
using namespace std;
int main(void)
{
    vector<int> v1(100, 0);
    array<int, 100> a1;
    cout << "size of v1: " << sizeof(v1) << endl;
    cout << "size of a1: " << sizeof(a1) << endl;
    return 0;
}

🚩 运行结果:

dd3dd0bfadc1300c92b0de71bc6e8a2e_60f0247b78f54d508f08c3a34edff49b.png

vector 是开在空间大的堆上的而 array 是开在寸土寸金的栈上的,堆可比栈的空间大太多太多了。


最尴尬的是 array 能做的操作几乎 vector 都能做,因为 vector 的存在 array 显得有些一无是处。


所以我们拿 array 去对标 vector 是不对的,拿去和原生数组比还是可以对比的。


但是 array 也只是封装过的原生数组罢了,是真的菜:

array<int, 100> a1;  // 封装过的原生数组
int a2[100];         // 原生数组

比起原生数组,array 的最大优势也只是有一个越界的检查,读和写都可以检查到是否越界。


要对比的话也只能欺负一下原生数组,然而面对强大的 vector,array 完全没有招架之力。


如何评价 array?在 C++11 增加完 array 后备受吐槽,从简化的角度来说完全可以不增加 array。


我的评价是 ——


" 十年磨一剑,但磨的是十年前的技术 "


🔺 总结:array 相较于原生数组,有越界检查之优势,实际中建议直接用 vector。


Ⅱ. 模板的特化(Template Specialization)


0x01 引入:给特殊类型准备特殊模板

通常情况下,使用模板可以实现一些与类型无关的代码


但是,对于一些特殊类型,可能我们就要对其进行一些 "特殊的处理" 。


💭 举例:如果不对特殊类型进行特殊处理就可能会出现一些问题,比如:

#include <iostream>
#include "Date.h"    /* 引入自己实现的日期类 */
using namespace std;
/* 判断左数是否比小于右数 */
template<class T>
bool Less(T left, T right) {
    return left < right;
}
int main(void)
{
    cout << Less(1, 2) << endl;        // 可以比较,结果正确
    Date d1(2022, 7, 7);
    Date d2(2022, 7, 8);
    cout << Less(d1, d2) << endl;      // 可以比较,结果正确
    Date* p1 = new Date(2022, 7, 16);
    Date* p2 = new Date(2022, 7, 15);
    cout << Less(p1, p2) << endl;      // 可以比较,结果正确
    return 0;
}

❓ 运行结果:(我们运行几次发现,其结果不稳定,对于 Date* 一会是真一会是假)

cb4c0ebf19271e4f8a242f76122ac2f0_19e18332f66e4c8cbcbadb80f954b4b4.png

问题出在没传指针,传的是 p1 和 p2,这里传 *p1 和 *p2 就能解决。但是……


这时候冲出个土匪拿炸弹强迫你:不让你传星号,就必须传 p1 和 p2,怎么办?


不慌,我们还可以用一种特殊的方式 —— 模板的特化,针对某些类型要进行特殊化处理。


0x01 模板特化的步骤

首先,必须要先有一个基础的函数模板。


其次,关键字 template 后面接上一对空的 <> 尖括号。


然后,函数名后跟一对尖括号,尖括号中指定需要特化的内容。


最后,函数形参表必须要和模板函数的基础参数类型完全相同,不同编译器会恶心人。


💬 代码演示:模板的特化

#include <iostream>
#include "Date.h"
using namespace std;
// 必不可少的原本
template<class T>
bool Less(T left, T right) {
    return left < right;
}
// 针对某些类型要特殊化处理 ———— 使用模板的特化解决
template<>
bool Less<Date*>(Date* left, Date* right) {
    return *left < *right;
}
int main(void)
{
    cout << Less(1, 2) << endl;       // 可以比较,结果正确
    Date d1(2022, 7, 7);
    Date d2(2022, 7, 8);
    cout << Less(d1, d2) << endl;     // 可以比较,结果正确
    Date* p1 = new Date(2022, 7, 16);
    Date* p2 = new Date(2022, 7, 15);
    cout << Less(p1, p2) << endl;     // 可以比较,结果正确  
    return 0;
}

🚩 运行结果:(运行多次后发现结果正常,每次都一致)

caac22c7b26d9f5db3aa9fe5ed67b346_2015de5a36c04140b54cee6612384562.png


💡 解读:对于普通类型,它还是会调正常的模板。对于 Date* 编译器就会发现这里有个专门为 Date* 而准备的特化版本,编译器会优先选择该特化版本。这,就是模板的特化。

4e11acbc482e73aeb22bd2d75fab7f47_2f06fd68f40e48de9f5bf8a7806f19de.png



当然了,函数内你可以自己设计如何去处理你想要处理的特殊情况,这都是你说的算:

template<>
bool Less<Date*>(Date* left, Date* right) {
    // 这里面你可以做各种处理
}

❓ 思考:现在我们加一个普通函数,Date* 会走哪个版本?

// 原模板
template<class T>
bool Less(T left, T right) {
    return left < right;
}
// 对模板特化的
template<>
bool Less<Date*>(Date* left, Date* right) {
    return *left < *right;
}
// 直接匹配的普通函数
bool Less(Date* left, Date* right) {
    return *left < *right;
}

🔑 答案:函数重载,会走直接匹配的普通函数版本,因为是现成的,不用实例化。你可以这么理解:原模板是生肉,模板特化是半生不熟的肉,直接匹配的普通函数是熟肉。


🔺 结论:函数模板不一定非要特化,因为在参数里面就可以处理,写一个匹配参数的普通函数也更容易理解。


0x02 类模板的特化

刚才函数模板不一定非要特化,因为可以写一个具体实现的函数。


但是类模板我们没法实现一个具体的实际类型,就必须要特化了。


💭 比如这里 d2 需要接收一个 int 和 一个 double 的值:

#include <iostream>
#include "Date.h"
using namespace std;
template<class T1, class T2>
class Data {
public:
  Data() {
  cout << "Data<T1, T2>" << endl;
  }
private:
  T1 _d1;
  T2 _d2;
};
int main(void)
{
  Data<int, int> d1;
  Data<int, double> d2;   // 需要特化解决
  return 0;
}

💬 代码演示:类模板的特化

template<class T1, class T2>
class Data {
public:
  Data() {
  cout << "Data<T1, T2>" << endl;
  }
private:
  T1 _d1;
  T2 _d2;
};
// 类模板的特化
template<>
class Data<int, double> {
public:
  Data() {
  cout << "Data<int, double>" << endl;
  }
};
int main(void)
{
  Data<int, int> d1;
  Data<int, double> d2;
  return 0;
}

🚩 运行结果:

39acaa3116d3c812de6de85b7f6c83cd_d5be107ac6ea437c95dc030754fec503.png


0x03 全特化和半特化

全特化:全特化即是将模板参数列表中所有的参数都确定化。


(我们刚才写的就是全特化)

...
// 全特化
template<>
class Data<int, double> {
public:
  Data() {
  cout << "Data<int, double>" << endl;
  }
};

半特化(又称偏特化):将部分参数类表中的一部分参数特化。


(半特化并不是特化一半,就像半缺省并不是缺省一半一样)

...
// 半特化(偏特化)
template<class T1>
class Data<T1, char> {
public:
  Data() {
  cout << "Data<T1, char>" << endl;
  }
};
int main(void)
{
  // 只要第二个值是 char 都会匹配到半特化
  Data<int, char> d3;
  Data<char, char> d4;
  return 0;
}

半特化还有一种表现方式,半特化可以用来对参数进行更进一步的限制。


💬 代码演示:限制两个参数都是指针

...
// 半特化还可以对参数进行进一步限制
template<class T1, class T2>
class Data<T1*, T2*> {
public:
  Data() {
  cout << "Data<T1*, T2*>" << endl;
  }
};
int main(void)
{
  // 只要你两个参数都是指针,就匹配
  Data<int*, char*> d5;
  Data<char*, string*> d6;
  Data<char**, void*> d7;
  return 0;
}


🚩 运行结果:

9b98125089ca024651c71d1c415cee8b_899d4b53f3f2415e8d9fecf9aa841111.png


💬 代码演示:限制两个参数都是引用

template<class T1, class T2>
class Data<T1&, T2&> {
public:
  Data() {
  cout << "Data<T1&, T2&>" << endl;
  }
};
int main(void)
{
  // 只要你两个参数都是引用,就匹配
  Data<int&, int&> d8;
  Data<char&, string&> d9;
  return 0;
}

🚩 运行结果:

c1338d515688f2b1bf474d1b389979ee_990e675b68df45afaa9047b67c1de8af.png


Ⅲ. 模板的优缺点


0x00 优点

① 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。


② 增强了代码的灵活性。


0x01 缺点

① 模板会导致代码膨胀问题,也会导致编译时间变长。


② 出现模板编译错误时,错误信息非常凌乱,不易定位错误。


相关文章
|
1月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
8月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
241 2
|
8月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
465 73
|
5月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
206 0
|
8月前
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
369 3
|
9月前
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
537 1
|
9月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
5月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
158 0
|
5月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
249 0
|
7月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
293 12