C++模板进阶——特化全解

文章目录

  • 引言
  • 一、特化的动机
  • 二、模板特化与重载的对比
    • [2.1 const 在 `*` 两侧的四态](#2.1 const 在 * 两侧的四态)
    • [2.2 Date* VS const Date*](#2.2 Date* VS const Date*)
    • [2.3 函数模板特化的局限性](#2.3 函数模板特化的局限性)
  • 三、类模板特化
    • [3.1 全特化](#3.1 全特化)
    • [3.2 偏特化------部分参数特化](#3.2 偏特化——部分参数特化)
    • [3.3 偏特化------结构限制特化](#3.3 偏特化——结构限制特化)
    • [3.4 全特化与偏特化的优先级](#3.4 全特化与偏特化的优先级)
  • 四、偏特化的深层坑
    • [4.1 指针偏特化里 const 的吸收](#4.1 指针偏特化里 const 的吸收)
    • [4.2 引用/指针偏特化里 T1 是底层类型](#4.2 引用/指针偏特化里 T1 是底层类型)
  • 五、非类型模板参数
  • 六、模板的分离编译

Ciallo(∠・ω< )⌒☆

C语言专栏

C语言博客_CSDN

数据结构专栏

数据结构博客_CSDN

C++专栏

C++博客_CSDN

作者仓库

代码仓库_gitee

引言

在之前介绍过的模板初阶里,我们学会了函数模板和类模板的基本写法。那种模板解决的是"通用逻辑"------不管传进来的是 intdouble 还是 Date,走同一套代码。

但通用模板不是万能的。当一个类型"长得比较特殊"(比如是指针、是引用、或者带 const)的时候,通用模板往往会给出错误的结果 。这篇《模板进阶》就来讲讲:模板特化------怎么给特殊类型"特殊处理",以及它背后一堆容易踩的坑。

一、特化的动机

先看一个最经典的例子。我写了一个通用的比较函数模板 LessFunc

cpp 复制代码
template<class T>
bool LessFunc(const T& left, const T& right)
{
	return left < right;   // 直接比 "<"
}
  • intdouble:比的是数值,正确。
  • Date 对象:Date 重载了 operator<,比的也是日期值,正确。
  • Date* 指针:出问题了 。这里比的是 left < right,也就是两个指针的地址谁大谁小,而不是它们指向的日期谁早谁晚。
text 复制代码
  日期对象比较(正确)        Date* 指针比较(出错)
  d1 = 2022-7-7               p1 --> d1 = 2022-7-7
  d2 = 2022-7-8               p2 --> d2 = 2022-7-8
  d1 < d2 比的是日期值        p1 < p2 比的是地址

这就是特化的动机:对某些特殊类型,通用模板的逻辑不对,需要单独"定制"一份。

二、模板特化与重载的对比

2.1 const 在 * 两侧的四态

这个坑的根源,其实是 const 放在 * 的哪一边,锁的完全不是同一个东西。我用一张表说清楚:

写法 含义 能否改指针 能否改指向的值
Date* 指向 Date
const Date* 指向 const Date 不能
Date* const const 指针 不能
const Date* const 指向 const 的 const 指针 不能 不能

一句话记:const* 左边,锁的是"指向的值";在 * 右边,锁的是"指针本身"。写代码时可以直接这么记:

cpp 复制代码
// const int *  : 不能通过这个指针改指向的值,但能改指针指向别处
// int * const  : 指针本身不能改,但能改它指向的值

2.2 Date* VS const Date*

明白了 const 的位置,再回头看函数模板特化。原模板是:

cpp 复制代码
template<class T>
bool LessFunc(const T& left, const T& right)

T = Date* 时,const T 其实是 Date* const------const*右边 ,锁的是指针。所以正确的特化形参必须是 Date* const&

cpp 复制代码
template<>
bool LessFunc<Date*>(Date* const& left, Date* const& right)
{
	return *left < *right;
}

而当 T = const Date* 时,const*左边 ,锁的是指向的值。特化形参就变成 const Date* const&

cpp 复制代码
template<>
bool LessFunc<const Date*>(const Date* const& left, const Date* const& right)
{
	return *left < *right;
}
text 复制代码
  原模板: bool LessFunc(const T& l, const T& r)
    T=Date*        ==>  Date* const&
    T=const Date*  ==>  const Date* const&

  特化<Date*>       使用 Date* const&          √
  特化<const Date*> 使用 const Date* const&    √
  写错              使用 const Date*&          ✘

我一开始就顺着 const 字面写成了 const Date*&,结果类型对不上 。因为 Date*(指向普通 Date 的指针)和 const Date*&(指向 const Date 的指针引用)根本是两种类型,编译器不会把这个特化匹配到 Date* 上。

2.3 函数模板特化的局限性

由此可见,函数模板特化对形参类型的要求极为苛刻const 位置、引用写法差一点就匹配不上,很容易出"看起来对、编译错"的坑。所以通用的建议是:

函数模板一般不建议特化,遇到处理不了的类型,直接写一个普通函数重载更省心、可读性更高:

cpp 复制代码
bool LessFunc(const Date* left, const Date* right)
{
	return *left < *right;
}

bool LessFunc(Date* left, Date* right)
{
	return *left < *right;
}

三、类模板特化

函数模板特化不推荐,但类模板特化 是真用得上的,而且能讲出更多名堂。先看一个模板类 Data

cpp 复制代码
template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};

3.1 全特化

全特化就是模板参数列表里的所有参数都确定下来了,单独写一份:

cpp 复制代码
template<>
class Data<int, double>
{
public:
	Data() { cout << "Data<int, double>" << endl; }
};

3.2 偏特化------部分参数特化

偏特化(也叫半特化)是"只把一部分参数固定,剩下的还留成模板参数":

cpp 复制代码
template<class T1>
class Data<T1, double>
{
public:
	Data() { cout << "Data<T1, double>" << endl; }
};

这里第二个参数固定成 double,第一个参数 T1 还留着。

3.3 偏特化------结构限制特化

偏特化不只是"固定部分参数",还能对参数做进一步的结构限制,比如限定参数必须是指针、必须是引用:

cpp 复制代码
// 两个参数都限定为指针
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
};

// 两个参数都限定为引用
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
	Data() { cout << "Data<T1&, T2&>" << endl; }
};

// T1 是引用、T2 是指针
template <typename T1, typename T2>
class Data <T1&, T2*>
{
public:
	Data() { cout << "Data<T1&, T2*>" << endl; }
	void Push(const T1& x) {}
};

3.4 全特化与偏特化的优先级

这里有个好玩的点。看这几个实例化:

cpp 复制代码
Data<int, int>      d1;   // 主模板
Data<int, char>     d2;   // 主模板
Data<int, double>   d3;   // 同时命中 全特化 和 偏特化
Data<char, double>  d4;   // 命中偏特化  Data<T1,double>
Data<char, char>    d5;   // 主模板
Data<char*, char*>  d6;   // Data<T1*,T2*>
Data<int*, char*>   d7;   // Data<T1*,T2*>
Data<int&, char&>   d8;   // Data<T1&,T2&>
Data<int&, double&> d9;   // Data<T1&,T2&>
Data<int&, int*>    d10;  // Data<T1&,T2*>

d3Data<int, double>,它同时能匹配全特化 Data<int,double>偏特化 Data<T1,double>(此时 T1=int)。两条路都通,编译器选谁?

text 复制代码
  Data<int, double> 实例化时, 能同时匹配:
     全特化: Data<int, double>        ★ 优先命中
     偏特化: Data<T1, double> (T1=int)
  → 全特化比偏特化更"具体", 优先选全特化

结论:全特化优先于偏特化,偏特化优先于主模板

四、偏特化的深层坑

4.1 指针偏特化里 const 的吸收

这是最容易绕晕的地方。看 Data<const int*, char*>:它匹配结构偏特化 Data<T1*, T2*> 时,T1 被推成什么?

cpp 复制代码
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
};
text 复制代码
  Data<const int*, char*> 匹配 Data<T1*, T2*>
     T1 = const int   ← const 被吸收进 T1
     T2 = char

注意:T1const int不是 int。本来 const int* 是指向 const 的指针,但结构模式 T1* 把那个 const 吸收成 T1 的一部分了。之所以这种结构特化有用,就是因为它能T1 吸收 const 来区分不同指针 ------const int*int* const 会对应到不同的 T1

4.2 引用/指针偏特化里 T1 是底层类型

再看 Data<int&, int*>,它匹配 Data<T1&, T2*>。这里 T1 到底是不是 int&

cpp 复制代码
template <typename T1, typename T2>
class Data <T1&, T2*>
{
public:
	Data() { cout << "Data<T1&, T2*>" << endl; }
	void Push(const T1& x) {}
};
text 复制代码
  Data<int&, int*> 匹配 Data<T1&, T2*>
     T1 = int        ← 引用被吸收, T1 是底层类型
     T2 = int
     真正引用在 T1&  --> int&

不是!T1int不是 int&T1& 才是 int&。也就是说,结构偏特化里的类型变量存的是"去掉结构修饰后的底层类型",真正的引用/指针/const 都体现在模式上,而不体现在类型变量里。

所以类里写 Push(const T1& x),当 T1=int 时其实是 Push(const int& x);而当实参是 Data<const int&, int*> 时,T1=const intPush 的形参就变成 const int&const 会折叠)。这些细节,就是偏特化最磨人的地方。

五、非类型模板参数

模板形参不一定是类型,还可以是常数std::array 就用它来定数组大小:

cpp 复制代码
template<class T, size_t N = 10>
class array
{
public:
	T& operator[](size_t index) { return _array[index]; }
	const T& operator[](size_t index) const { return _array[index]; }
	size_t size() const { return _size; }
	bool empty() const { return 0 == _size; }
private:
	T _array[N];
	size_t _size;
};

array<int, 100> 就是一个 100 个元素的数组;N 是编译期就能确定的常数。非类型模板参数必须在编译期确定;浮点数、类对象、字符串在 C++17 之前不能用作非类型模板参数,C++20 开始允许浮点。

六、模板的分离编译

模板的声明和定义如果分开放在两个 .cpp 里,链接会失败 。比如 a.h 里只声明 template<class T> T Add(const T&, const T&)a.cpp 里写定义,main.cpp 里调用------因为模板要遇到实际类型才能"实例化"出代码,而 a.cpp 编译时不知道 main 会实例化 int 还是 double,所以链接时报"无法解析的外部符号"。

解决办法是把声明和定义都放进 .h / .hpp,或者反过来用"显式实例化"把类型提前告诉编译器。

相关推荐
晓得迷路了1 小时前
栗子前端技术周刊第 145 期 - Remix 3 RC、htmx 4.0、Rslib 1.0...
前端·javascript·react.js
励志不掉头发的内向程序员1 小时前
【LibreCAD 2D架构】从两个坐标到图形实体:RS_ActionDrawLine如何创建RS_Line
开发语言·c++·qt·学习·系统架构
雪芽蓝域zzs1 小时前
第三十五节:Axios 统一错误拦截、401 Token 过期处理
前端·javascript·vue.js
j7~1 小时前
【C++】C++的类型转换--详解
c++·static_cast·const_cast·rtti·c语言类型转换·c++强制类型转换
范什么特西2 小时前
一些常用名词
开发语言·前端·javascript
牧瀬クリスだ2 小时前
前端三件套基础之JS!!!!
前端·javascript
乌夷2 小时前
JavaScript 的事件循环
开发语言·javascript·ecmascript
单线程_012 小时前
从案例分析 Vue3 Tokenizer+Parser 源码四
前端·javascript·vue.js
计科杨某人2 小时前
简单算法题(基础入门题)
c++·算法·题解·入门·基础算法