C++ 模板进阶全解析:非类型模板参数、模板特化与分离编译详解

目录

  • 前言
  • 一、非类型模板参数
    • [1.1 概念](#1.1 概念)
    • [1.2 非类型模板参数在STL库中的应用](#1.2 非类型模板参数在STL库中的应用)
      • [1.2.1 非类型模板模板参数在array中的应用](#1.2.1 非类型模板模板参数在array中的应用)
        • [1.2.1.1 array相比原生数组的优势](#1.2.1.1 array相比原生数组的优势)
  • 二、模板的特化
    • [2.1 概念](#2.1 概念)
    • [2.2 函数模板特化](#2.2 函数模板特化)
    • [2.3 函数模板特化的大坑](#2.3 函数模板特化的大坑)
    • [2.3 类模板特化](#2.3 类模板特化)
      • [2.3.1 全特化](#2.3.1 全特化)
        • [2.3.1.1 vector< bool > 是什么?](#2.3.1.1 vector< bool > 是什么?)
        • [2.3.1.2 相比普通 vector 存布尔值的优势](#2.3.1.2 相比普通 vector 存布尔值的优势)
        • [2.3.1.3 vector< bool > 接口与通用 vector 的核心变化](#2.3.1.3 vector< bool > 接口与通用 vector 的核心变化)
        • [2.3.1.4 flip () 接口的作用](#2.3.1.4 flip () 接口的作用)
        • [2.3.1.5 从 vector< bool > 看模板特化的价值](#2.3.1.5 从 vector< bool > 看模板特化的价值)
      • [2.3.2 偏特化](#2.3.2 偏特化)
        • [2.3.2.1 偏特化的特殊用法](#2.3.2.1 偏特化的特殊用法)
      • [2.3.3 类模板特化应用示例](#2.3.3 类模板特化应用示例)
        • [2.3.3.1 左值右值和const的绑定优先级](#2.3.3.1 左值右值和const的绑定优先级)
  • 三、模板分离编译
    • [3.1 什么是分离编译](#3.1 什么是分离编译)
    • [3.2 模板的分离编译](#3.2 模板的分离编译)
    • [3.3 解决方法](#3.3 解决方法)
  • 四、模板总结
  • 结语

🎬 云泽Q个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列

⛺️遇见安然遇见你,不负代码不负卿~


前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、非类型模板参数

1.1 概念

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

类型形参即:出现在模板参数列表中,跟在class或者typename之后的参数类型名称

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

在不知道非类型模板参数之前,N的定义如图使用define

这个写法的的问题就是,假设现在有两个栈,为了存第二个栈的1000个数据,N就要定义为1000,此时对于第一个栈就会浪费990个空间。所以说对于用宏来定义常量决定数组大小是不太好的,主要是无法灵活的控制

所以C++给了一个非类型模板参数用来定义常量,这时候就灵活的解决了这样的问题

复制代码
template<class T, size_t N>
          类型模板参数  非类型模板参数(必须是常量)

虽然st1和st2的T都是int,但实例化出的是两个不同的类型,因为第二个模板参数不一样

注意:

  1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的
  2. 非类型的模板参数必须在编译期就能确认结果

1.2 非类型模板参数在STL库中的应用

1.2.1 非类型模板模板参数在array中的应用

std::array 是一个类模板,其定义的核心就是依赖非类型模板参数来限定数组大小

  • 类型模板参数 T:指定数组存储的元素类型(如 int、std::string);
  • 非类型模板参数 N :指定数组的固定长度 ,要求是编译期可确定的常量表达式

补充:array是不支持尾插尾删,头插头删的,因为其数组大小是固定的

1.2.1.1 array相比原生数组的优势
cpp 复制代码
#include<iostream>
#include<array>
#include<list>
using namespace std;

void func(int* a)
{
	//不能使用范围for
	//for (auto e : a)
	//{
	//	cout << e << " ";
	//}
	//cout << endl;
}

void func(const array<int, 10>& a)
{
	//能使用范围for
	for (auto e : a)
	{
		cout << e << " ";
	}
	cout << endl;
}

int main()
{
	// array对内置类型做参数默认不会初始化
	// 对自定义类型调默认构造
	array<int, 10> a1;
	a1[3] = 3;
	a1[9] = 9;
	for (auto e : a1)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << sizeof(a1) << endl;

	int a2[10];
	a2[3] = 3;
	a2[9] = 9;
	for (auto e : a2)
	{
		cout << e << " ";
	}
	cout << endl;

	list<array<int, 10>> lt;
	func(a1);
	func(a2);
	return 0;
}

结合代码来说:
优势 1:传参时不会丢失大小信息,支持范围 for 循环

  • 代码中 func(a2) 时,a2 退化为 int*(因为在C语言的角度,数组的传递效率太低了,不允许传递数组),此时 func 接收的是一个指针,编译器不知道指针指向的内存有多长,因此范围 for 无法遍历(范围 for 需要知道遍历的 "起点" 和 "终点");
  • func(a1) 传的是 const array<int,10>&,a1 的类型是 std::array<int,10>,内部封装了 begin()/end() 迭代器,范围 for 能通过迭代器确定遍历边界,因此可以正常执行。

优势 2:更强的安全性

  • a1 可以用 a1.at(10) 访问越界下标,此时程序会抛出 std::out_of_range 异常,能快速定位越界问题;
  • a2[10] 访问越界时,编译器无任何提示,运行时可能崩溃或篡改内存,调试难度大。

优势 3:兼容 STL 容器和算法,编程更便捷

  • 代码中 list<array<int, 10>> lt 是完全合法的,因为 std::array 是 "可拷贝、可赋值的完整类型";
  • 若尝试写 list<int[10]> lt,会直接编译报错 ------ 原生数组不能作为容器的元素(无法拷贝、赋值);
  • 此外,对 a1 排序只需 sort(a1.begin(), a1.end()),对 a2 则需 sort(a2, a2+10),后者需要手动计算终点,容易因写错长度(比如写成 a2+9)导致逻辑错误。

优势 4:大小获取

  • 提供 size() 成员函数(a1.size() 直接得 10),且对于array这样的静态数组,开空间是编译式开辟,性能消耗更小,对于原生数组来说,动态开辟更麻烦一些
  • 需手动计算 sizeof(a2)/sizeof(a2[0]),传参后无法计算

此外,array容器中还有一个成员函数叫做fill,array的静态数组默认没有初始化,fill就可以将数组中所有元素统一赋值为指定的某个值,替换掉数组中原有的所有元素值

所以在C++中若是使用静态数组,还是很推荐使用array

二、模板的特化

2.1 概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需特殊处理,比如:实现了一个专门用来进行小于比较的函数模板

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{
	}

	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}

	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}

	friend ostream& operator<<(ostream& _cout, const Date& d);
private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}

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()
{
	cout << Less(1, 2) << endl;//可以比较,结果正确

	Date* p1 = new Date(2025, 1, 1);
	Date* p2 = new Date(2025, 1, 3);
	cout << Less(p1, p2) << endl;//可以比较,结果错误
	return 0;
}

上面的Less在绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。示例中,p1指向的对象显然小于p2指向的对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误

此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式 。模板特化中分为函数模板特化类模板特化

2.2 函数模板特化

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同,编译器可能会报一些奇怪的错误

补充三个细节:

  • 普通函数模板中,当 T 推导为 Date* 时,left < right 是指针地址比较,不调用 Date 类重载的 operator<
  • Less<Date*> 特化版本中,*left < *right 是Date 对象比较,会调用 Date 类重载的 operator<
  • 代码中当前的 Less(p1, p2) 理论上会匹配特化版本(因为模板参数推导为 Date*,特化版本正好是 Less<Date*>),如果没匹配到,可能是编译器语法细节问题,可显式指定模板参数:
cpp 复制代码
// 显式指定特化版本,确保调用
cout << Less<Date*>(p1, p2) << endl; // 此时会调用特化版本,输出1(正确)

2.3 函数模板特化的大坑

其次,函数模板的特化有一个很坑的点,前面的函数模板和函数模板特化使用的是传值传参,但是在C++中一般为了效率使用的是const 引用传参,但是像这样指针特化使用const引用就会报错

下面我把这个问题拆解清楚

  1. 心规则:函数模板特化的 "签名匹配要求"
    C++ 明确规定:函数模板的特化版本,其参数列表必须和原模板实例化后的参数类型完全一致(仅替换模板参数T,不能修改const、引用(&)的绑定方式,也不能改变const修饰的对象)。

我们先对比原模板和特化版本的参数类型,就能直观看到不匹配的问题:
步骤 1:分析原模板实例化后的参数类型(T=Date * 时)

cpp 复制代码
template<class T>
bool Less(const T& left, const T& right) // 两个参数都是 const T&(const + T类型 + 引用)
{
	return left < right;
}

当调用Less(p1, p2)(p1/p2是Date*类型),模板参数T会被推导为Date*,此时原模板的参数类型会被实例化为:

  • const T& → const (Date*) & → 注意:C++ 中const修饰指针时,const T(T = 指针)表示指针本身是 const 的(Date* const),而非 "指向 const 对象的指针"(const Date*)
  • 所以原模板的真实参数类型是:Date* const & left、Date* const & right(对 "const 指针" 的引用)。

步骤 2:分析特化版本参数类型(完全不匹配)

cpp 复制代码
template<>
bool Less<Date*>(const Date*& left, const Date*& right)
// 参数类型:const Date*&(对"指向const Date对象的指针"的引用)
{
	return *left < *right;
}

这里的致命问题:

  • 类型不匹配 :原模板是Date* const &(const 修饰 "指针本身"),特化版本是const Date*&(const 修饰 "指针指向的对象"),二者是完全不同的类型;

修正后的写法

注意:这个问题在平时写代码的时候是不容易发现的,且由于函数模板和普通函数是可以同时存在的,且优先匹配匹配度更高的,所以一般情况下为了避免使用函数模板在这种情况下出错,通常是将该函数直接给出

这里参数是不能写为const Date* 的,若写为const Date* ,由于Date* 到const Date* 还要发生类型转换,就依旧使用上面的函数模板了

2.3 类模板特化

2.3.1 全特化

类模板特化的时候,内部的成员并不要求一致,成员函数成员变量随便写,也就是说原模版定义的,特化版本可以不定义,也可以新增,可以将其看作一个全新的类

2.3.1.1 vector< bool > 是什么?

库中的vector除了有个原模板外,还有一个vector< bool >,vector< bool >就是vector原模板对布尔类型的一个特化版本,核心设计目标是位压缩存储布尔值 ------ 因为普通的布尔值(bool类型)在内存中默认占 1 字节(8 位),但实际上只需要 1 位就能表示true/false,vector< bool >通过位操作把每个布尔值压缩到 1 位,大幅节省内存。

2.3.1.2 相比普通 vector 存布尔值的优势

如果不用vector< bool >,你可能会用vector< char >/vector< uint8_t >/vector< bool >(非特化,实际等价于按字节存储)来存布尔值,vector< bool >的核心优势是:

⚠️ 注意:vector< bool >的访问速度略慢(因为需要位运算解析),但空间优势在大数据量场景下远大于这点性能损耗,是典型的 "空间换时间"(或 "空间优先")的优化。

2.3.1.3 vector< bool > 接口与通用 vector 的核心变化

vector< bool >的接口大部分和通用vector一致(如size()、push_back()、resize()等),但因为位压缩存储的特性,它的元素并非真正的bool对象,而是通过代理对象(vector< bool >::reference) 访问,这会导致一些关键接口的变化

2.3.1.4 flip () 接口的作用

vector< bool >额外提供了flip()成员函数(通用vector无此接口),是针对位操作优化的专属接口,核心作用是翻转布尔值(true↔false),有两个版本:

  1. 无参版本:翻转所有元素
cpp 复制代码
#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<bool> v{true, false, true};
    v.flip(); // 翻转所有元素
    for (auto b : v) {
        cout << boolalpha << b << " "; // 输出:false true false
    }
    return 0;
}
  1. 带迭代器参数版本:翻转指定位置元素
cpp 复制代码
vector<bool> v{true, false, true};
v.flip(v.begin() + 1); // 翻转第2个元素
for (auto b : v) {
    cout << boolalpha << b << " "; // 输出:true true true
}

💡 为什么需要flip()?

底层是位存储,翻转操作可通过位异或(^= 1) 高效完成,比手动遍历v[i] = !v[i]更高效(尤其是批量翻转),且语义更清晰。

2.3.1.5 从 vector< bool > 看模板特化的价值
  1. 针对特殊类型做极致优化
    通用vector< T >的设计是 "通用化" 的 ------ 每个元素占sizeof(T)字节,无法适配bool仅需 1 位的特性。特化后通过位压缩,最大化节省内存,这是通用版本无法做到的。
  2. 提供专属优化接口
    特化版本可根据类型特性增加专属接口(如flip()),利用底层实现的优势(位操作)提供更高效、更贴合场景的功能,提升开发效率和程序性能。

特化版本可以认为是一个接近成品的东西,在类型匹配的情况下,有原模版和特化版本的时候会优先走特化版本,因为特化版本的类型相当于已经处理好了,基本上不需要实例化

2.3.2 偏特化

全特化就是所有模板参数都特化,偏特化在有些书上也叫半特化,就是特化部分模板参数

当偏特化和全特化有冲突时,优先匹配全特化,优先匹配实例化参数更多的那个

cpp 复制代码
//偏特化/半特化
template<class T1>
class Data<T1, double>
{
public:
	Data() { cout << "Dara<T1, double> 特化" << endl; }
};
2.3.2.1 偏特化的特殊用法

这里最灵活的点就是,虽然参数是指针才能匹配,但是T1, T2还是保留了非指针类型,这时候特化的类内想用原类型就直接用T,想用指针类型就用T*

除了是指针,还可以是引用,模板参数是可以传引用的,甚至可以指针和引用混在一起,一个是指针,一个是引用

2.3.3 类模板特化应用示例

类模板的特化也可以应用在前面仿函数中,前面的文章是重写了一个可以解引用来比较日期类大小的仿函数修正指针类型优先级队列比较逻辑,这里就可以特化日期类中的仿函数

priority_queue.h

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 666
#pragma once
#include<vector>
#include<iostream>
using namespace std;

namespace yunze
{
	template<class T>
	struct Less
	{
		bool operator() (const T& x, const T& y) const
		{
			return x < y;
		}
	};

	template<class T>
	struct Greater
	{
		bool operator() (const T& x, const T& y) const
		{
			return x > y;
		}
	};

	template<class T, class Container = std::vector<T>, class Compare = Less<T>>
	class priority_queue
	{
	public:
		template<class InputIterator>
		priority_queue(InputIterator first, InputIterator last)
			:_con(first, last)
		{
			//建堆
			for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
			{
				adjust_down(i);
			}
		}

		//强制编译器生成默认构造
		priority_queue() = default;

		void adjust_up(int child)
		{
			Compare com;
			size_t parent = (child - 1) / 2;
			while (child > 0)
			{
				//if (_con[child] > _con[parent])
				//if (_con[parent] < _con[child])
				if (com(_con[parent], _con[child]))
				{
					swap(_con[child], _con[parent]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else {
					break;
				}
			}
		}

		void adjust_down(int parent)
		{
			Compare com;
			//假设法,默认左孩子大
			size_t child = parent * 2 + 1;
			while (child < _con.size())
			{
				//if (child + 1 < _con.size() && _con[child + 1] > _con[child])
				//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
				if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
				{
					child++;
				}
				//if (_con[child] > _con[parent])
				//if (_con[parent] < _con[child])
				if (com(_con[parent], _con[child]))
				{
					swap(_con[child], _con[parent]);
					parent = child;
					child = parent * 2 + 1;
				}
				else {
					break;
				}
			}
		}

		void push(const T& x)
		{
			_con.push_back(x);
			adjust_up(_con.size() - 1);
		}

		void pop()
		{
			//用front和back也可以,容器一般都支持这两个接口
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			adjust_down(0);
		}

		const T& top() const
		{
			return _con[0];
		}

		bool empty() const
		{
			return _con.empty();
		}

		size_t size() const
		{
			return _con.size();
		}
	private:
		Container _con;
	};
}

test.cpp

cpp 复制代码
#include"priority_queue.h"

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{
	}

	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}

	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}

	friend ostream& operator<<(ostream& _cout, const Date& d);
private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}

namespace yunze
{
	//特化版本
	template<>
	struct Less<Date*>
	{
		bool operator() (const Date*& x, const Date*& y) const
		{
			return x < y;
		}
	};

	//特化版本
	template<>
	struct Greater<Date*>
	{
		bool operator() (const Date*& x, const Date*& y) const
		{
			return x > y;
		}
	};
}

int main()
{
	yunze::priority_queue<Date*> pq;
	pq.push(new Date(2025, 1, 1));
	pq.push(new Date(2025, 1, 3));
	pq.push(new Date(2025, 1, 2));
	while (!pq.empty())
	{
		cout << *pq.top() << " ";
		pq.pop();
	}
	cout << endl;

	return 0;
}

这里yunze::priority_queue<Date*> pq;会实例化类模板,pq.push调用adjust_uppriority_queueadjust_up/adjust_down会调用特化的 Less<Date*>(因为 priority_queue 默认比较器是Less< T >,这里 T 是 Date*

但是这里还会出现一个很隐蔽的问题就是传参传不过去,有的兄弟可能说了,这里不是特化不匹配的问题吗?
类模板的特化(完全 / 偏特化),其模板参数数量由原始类模板的参数数量决定;而函数模板只有完全特化(无偏特化),其特化的参数数量也由原始函数模板的参数数量决定

且再补充一个语法细节,template<>内不用加class T的原因:

  • 通用模板(Primary Template):template< class T > 中的 < class T > 是声明模板参数,表示模板依赖一个 "未确定的类型 T",需要实例化时指定;
  • 完全显式特化(Full Explicit Specialization):template<> 空尖括号表示所有模板参数都已被固定为具体类型,没有需要声明的 "未确定参数",因此尖括号内为空。

模板的核心是 "代码生成的蓝图",通用模板和特化模板的template声明逻辑完全不同:
(1)通用模板:声明 "待绑定的模板参数"

cpp 复制代码
// 通用模板:template<class T> 声明"我有一个未确定的参数T"
template<class T>
struct Less
{
    bool operator() (const T& x, const T& y) const { return x < y; }
};

这里的template< class T >是告诉编译器:

  • 这个Less结构体是一个 "模板",不是具体类型;
  • 它依赖一个名为T的类型参数,只有当实例化(比如Less< int >Less<Date*>)时,T才会被替换为具体类型。

(2)完全显式特化:绑定所有模板参数,无需声明

cpp 复制代码
// 完全特化:template<> 表示"所有模板参数已固定,无待绑定参数"
template<>
struct Less<Date*>
{
    bool operator() (const Date*& x, const Date*& y) const { return *x < *y; }
};

这里的逻辑是:

  • 我们明确指定了 "通用模板的T被固定为Date*",<Date*>就是对通用模板中唯一参数T的 "最终绑定";
  • 此时这个Less<Date*>已经是一个确定的、完整的类型,不再依赖任何未确定的模板参数,因此template后的尖括号不需要再声明class T(声明了反而多余,且不符合语法)。

(3)反例:错误写法的编译器行为(印证语法规则)

如果强行在特化时写template< class T >,会触发编译错误,比如:

cpp 复制代码
// 错误写法:完全特化却声明了模板参数T
template<class T> // ❌ 编译器报错:单参数模板的完全特化不能有模板参数
struct Less<Date*>
{
    bool operator() (const Date*& x, const Date*& y) const { return *x < *y; }
};

编译器会判定这是 "非法的偏特化"------ 因为偏特化的核心是 "固定部分参数,保留部分参数",但单参数模板无法 "保留部分参数",因此必须用template<>表示完全特化。

(4)补充:偏特化的对比(辅助理解)

为了更清晰,我们对比 "偏特化"(Partial Specialization)的语法 ------ 偏特化是 "固定部分模板参数,保留部分参数",因此需要声明保留的参数:

cpp 复制代码
// 偏特化:固定T为"指针类型",但保留T作为未确定参数
template<class T> // 这里需要声明T,因为还保留了未确定的参数
struct Less<T*> 
{
    bool operator() (const T*& x, const T*& y) const { return *x < *y; }
};

这个偏特化的逻辑是:

  • 我们只固定了 "T 是指针类型",但T本身还是未确定的(比如Less<int*>Less<Date*>都匹配这个偏特化);
  • 因此需要template< class T >声明保留的参数T,这和 "完全特化无保留参数,故 template<> 为空" 形成鲜明对比。

说的有点多,继续说传参传不过去的细节问题,com(_con[parent], _con[child]),这里vector内的数据类型是Date*const Date*& x, const Date*& y,const修饰的是:指针指向的 Date 对象 (即通过这个指针,不能修改它指向的 Date 对象的内容),而Date*传给const Date*会发生类型转换,类型转换会产生临时变量,同时也是一个权限的缩小,权限的缩小是没有问题的,而临时变量又具有常性,而const Date*& x, const Date*& y又是普通引用,所以这里就会出现参数匹配的问题

2.3.3.1 左值右值和const的绑定优先级

这部分很绕人,下面再拆开细说
第一步:先理清const的绑定优先级

C++ 类型解读的黄金法则:从右往左读,const 永远绑定离它最近的类型

类型:通用模板const T&

代入 T=Date * 后的实际类型:Date* const&

从右往左读:引用 → const 的指针 → 指向 Date、

const 修饰目标:指针本身(不可改)

引用类型本质:const 左值引用

类型:手写const Date*&

代入 T=Date * 后的实际类型:const Date*&

从右往左读:引用 → 指针 → 指向 const 的 Date

const 修饰目标:指针指向的 Date 对象(不可改)

引用类型本质:普通左值引用

第二步:左值 / 右值 + 引用绑定规则(核心)

类型:左值

通俗定义:能取地址、能放在赋值号=左边的对象

典型例子:1. 普通变量(int a = 10; 中的a) 2. 数组元素(_con[0]) 3. 指针变量(Date* p = new Date(); 中的p)

类型:右值

通俗定义:不能取地址、只能放在赋值号=右边的临时对象

典型例子:1. 字面量(10、"abc") 2. 临时对象(Date(2025,1,1)、a+b的结果) 3. 类型转换产生的临时值(Date*const Date*的临时指针)

为什么「const Date*&」不是 const 左值引用?

"const 左值引用" 的核心定义是:引用本身绑定的对象(而非对象内部)是 const 的。

  • Date* const&:引用绑定的是「const 的 Date * 指针」(指针本身不能改)→ 符合 "const 左值引用" 的定义;
  • const Date*&:引用绑定的是「普通的 const Date * 指针」(指针本身可以改,只是指向的 Date 对象不能改)→ 引用绑定的对象(指针)本身不是 const 的,因此是普通左值引用,而非 const 左值引用。

左值引用的绑定规则(决定传参是否合法)

  • 普通左值引用(如const Date*&),只能绑定左值,不能绑定右值,实参Date*const Date*生成临时右值 → 绑定失败(报错)
  • const 左值引用(如Date* const&) 既能绑定左值,也能绑定右值 即使生成临时右值,也能绑定 → 传参合法

这里有三种解决方法,各自的特点如下:
方法 1:将参数改为Date* const&(推荐,符合引用传参习惯)

cpp 复制代码
template<>
struct Less<Date*>
{
    // 关键修改:const移到*右边 → Date* const&
    bool operator() (Date* const& x, Date* const& y) const
    {
        return *x < *y; // 比较逻辑不变,仍解引用Date对象
    }
};

核心原理

  • Date* const&中,const绑定在*右侧 → 修饰指针本身,成为「const 左值引用」;
  • const 左值引用的规则:既能绑定左值(如_con[child]),也能绑定临时右值(如Date*const Date*的临时指针);
  • 改完后传参时,即使有类型转换生成临时值,也能正常绑定,不会报错。

方法 2:参数改为值传递const Date*(更简单,指针场景首选)

cpp 复制代码
template<>
struct Less<Date*>
{
    // 关键修改:直接传值 → const Date*
    bool operator() (const Date* x, const Date* y) const
    {
        return *x < *y; // 比较逻辑不变
    }
};

核心原理

  • 指针本身是 "地址值",占用 4/8 字节(32/64 位系统),值传递的拷贝成本极低,几乎无性能损耗;
  • 值传递不需要绑定引用,而是直接拷贝实参(或临时对象)到形参,绕开了 "引用绑定右值" 的规则限制;
  • 这种写法是指针传参的 "极简安全款",新手优先选,不用纠结 const 和引用的组合。

方法 3:参数改为const Date* const&(双 const,最严格的安全版本)

  1. 类型拆解(从右往左读)
    const Date* const&的完整含义是:
    引用 → const 的指针 → 指向 const 的 Date 对象

这里包含两个const,各自的修饰目标是:

  • 右边的const(紧贴*):修饰指针本身(指针不能指向其他对象);
  • 左边的const(紧贴Date):修饰指针指向的 Date 对象(不能通过指针修改 Date 的内容)。
cpp 复制代码
template<>
struct Less<Date*>
{
    // 双const:既锁指针,又锁Date对象
    bool operator() (const Date* const& x, const Date* const& y) const
    { 
        return *x < *y; // 比较逻辑不变(解引用Date对象)
    }
};

核心原理

这个写法能解决报错的关键是:右边的const(紧贴*)让它成为「const 左值引用」 ------ 符合 "const 左值引用可以绑定临时右值" 的规则,因此Date转const Date生成的临时右值能正常绑定,传参不再报错。

同时,它比方法 1(Date* const&)多了一层安全限制:

  • 方法 1 的Date* const&:只能锁指针本身(指针不能改),但可以通过指针修改 Date 对象(比如*x = Date(2026,1,1));
  • 方法 3 的const Date* const&:既锁指针本身,又锁指针指向的 Date 对象(既不能改指针,也不能通过指针改 Date),是最严格的只读安全版本。

当然还有一种更绝的方法,不过要结合具体需求,这里可以使用偏特化

直接放弃用指针比较,而是直接按指针指向的内容比较,当然在需要指针比较的场景就坑了

三、模板分离编译

3.1 什么是分离编译

3.2 模板的分离编译

3.3 解决方法

四、模板总结


结语

相关推荐
沐知全栈开发1 小时前
FastAPI 安装指南
开发语言
2501_930707782 小时前
使用C#代码在 Word 中删除页眉或页脚
开发语言·c#·word
坚持学习前端日记2 小时前
后台管理系统文档
java·开发语言·windows·spring boot·python·spring
凯哥Java2 小时前
MaxKB4J:基于Java的高效知识库问答系统与工作流智能解决方案
java·开发语言
我是小疯子662 小时前
C++ODB实战指南:高效ORM开发
c++
悟能不能悟2 小时前
Postman Pre-request Script 详细讲解与高级技巧
java·开发语言·前端
txinyu的博客2 小时前
Reactor 模型全解析
java·linux·开发语言·c++
IMPYLH2 小时前
Lua 的 Package 模块
java·开发语言·笔记·后端·junit·游戏引擎·lua
Ulyanov2 小时前
Python射击游戏开发实战:从系统架构到高级编程技巧
开发语言·前端·python·系统架构·tkinter·gui开发