前言

在现代 C++ 中,模板(Template)是一项极其强大的特性。
它使得我们可以编写与类型无关的代码,从而实现高复用性、高灵活性和高性能的泛型编程

本篇文章将系统讲解以下三个模板进阶知识点:

  1. 非类型模板参数

  2. 模板特化

    • 函数模板特化
    • 类模板特化(含偏特化)
  3. 模板的分离编译


一、非类型模板参数

1.1 概念介绍

在模板参数中,除了常见的类型参数(typename / class)外,还可以使用常量值作为模板参数,这种参数称为 非类型模板参数

template<class T, size_t N>
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 N; }

private:
    T _array[N];
};

这里的 N 就是一个非类型模板参数,它在编译期间必须是可确定的常量。


1.2 使用注意事项

限制项 说明
必须是编译期常量 不能是运行时变量
不允许的类型 浮点数、类对象、字符串常量
常见类型 整型、枚举、指针、引用等

示例:

Array<int, 10> arr;    //正确,10 是编译期常量
Array<double, 3.14> a; //错误,浮点数不能作为非类型模板参数

1.3 易错概念辨析

提问: 下面代码中的s1s2是同一个类型吗?

template<class T = int, size_t N = 100>
class Stack
{
private:
	T _a[N];
};
int main()
{
	Stack<int, 10> s1;
	Stack<int, 1000> s2;
	return 0;
}

不是同一个类型!
二者只是使用了同一个模板,在编译过程中会实例化为两个不同的类
在vs2022中,软件识别s1的类型为Stack<int, 10U>s2的类型为Stack<int, 1000U>
说明虽然我们只是往模板里传入两个不同的值,但是编译器会根据传入的值,将模板实例化为不同的类型
简而言之,是让编译器完成了本该我们手写的工作。


二、模板特化

2.1 为什么需要特化?

模板的设计初衷是通用性,但有些类型的行为特殊,需要额外处理。
例如指针类型的比较操作,如果直接使用 <,比较的是地址而不是内容,这时就需要“特化”模板。

模板特化就是针对特定类型重新定义模板的实现方式。


2.2 函数模板特化

以下是一个普通的比较函数模板:

template<class T>
bool Less(T left, T right) {
    return left < right;
}

对于普通类型(如 intdouble)没问题;
但若传入指针类型,则比较的是内存地址!

template<class T>
bool Less(T left, T right)
{
	return left < right;
}
int main()
{
	cout << Less(1, 2) << endl; // 可以比较,结果正确
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl; // 可以比较,结果正确
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 可以比较,结果错误
	return 0;
}

此处比较的是开辟的内存空间的地址的大小,因此每次运行结果都是随机的,因为开辟的地址不一定按顺序开辟。

解决办法:函数模板特化

编译器在选择使用哪个方法时,会优先选择最合适的进行使用(类似于运算符重载时选择匹配的参数列表),如果没有实现对应的类型,那么就会走函数模板进行实例化。然而,如果有已经实现好的模板函数(也就是特化好的模板函数),那么编译器会优先使用实现好的,类似下面的代码:传入Date*类型的值时,会直接使用这个特化好的函数

template<>
bool Less<Date*>(Date* left, Date* right) {
    return *left < *right; // 比较指针指向的对象
}

使用示例:

Date d1(2022, 7, 7), d2(2022, 7, 8);
Date* p1 = &d1;
Date* p2 = &d2;

cout << Less(p1, p2); // 调用特化版本

小提示:
对于复杂函数模板,函数模板特化编写成本高。
如果某个类型确实特殊,直接写一个普通函数往往更简洁。


2.3 类模板特化

类模板特化主要分为:

类型 描述
全特化(Full Specialization) 所有模板参数都被确定
偏特化(Partial Specialization) 只固定部分参数或对参数模式进行限定

(1)全特化

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

// 全特化
template<>
class Data<int, char> {
public:
    Data() { cout << "Data<int, char>" << endl; }
private:
    int _d1;
    char _d2;
};

测试结果:

Data<int, int> d1;   // 调用通用模板
Data<int, char> d2;  // 调用全特化模板

输出:

Data<T1, T2>
Data<int, char>

(2)偏特化

偏特化是指只“部分特化”模板参数,或限定某种类型模式。

示例1:对第二个模板参数特化为 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; }
};

示例2:特化为指针类型

template<typename T1, typename T2>
class Data<T1*, T2*> {
public:
    Data() { cout << "Data<T1*, T2*>" << endl; }
};

示例3:特化为引用类型

template<typename T1, typename T2>
class Data<T1&, T2&> {
public:
    Data(const T1& d1, const T2& d2)
        : _d1(d1), _d2(d2) {
        cout << "Data<T1&, T2&>" << endl;
    }
private:
    const T1& _d1;
    const T2& _d2;
};

调用结果表:

调用语句 实际调用版本
Data<int, double> d1; 基础模板
Data<double, int> d2; 偏特化(T2 = int)
Data<int*, int*> d3; 偏特化(指针版)
Data<int&, int&> d4(1,2); 偏特化(引用版)

2.4 类模板特化应用案例

在 STL 中,std::less 是一个常见的比较器类。下面模拟其简化实现:

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

但是,当我们用 Less<Date*> 比较日期指针时,会比较地址。
这时就可以使用类模板特化

template<>
struct Less<Date*> {
    bool operator()(Date* x, Date* y) const {
        return *x < *y;
    }
};

这样,只要Date类里面支持<的运算符重载,就可以正确比较日期类的大小
结果:

比较对象 结果
Less<Date>()(d1, d2) 比较对象内容
Less<Date*>()(p1, p2) 比较指针指向的对象

三、模板的分离编译

3.1 普通分离编译

C++ 通常将声明放在 .h 文件,定义放在 .cpp 文件,最后链接生成可执行文件。
但模板不同,编译器需要在实例化时看到完整定义


3.2 问题示例

// 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);
}

上面代码会报错:

编译器无法在实例化模板时找到定义。


3.3 解决方案

方法 说明 是否推荐
将声明与定义放在同一头文件中 通常使用 .hpp 或直接放 .h 推荐
显式实例化(Explicit Instantiation) .cpp 中显式声明具体类型 不推荐

显式实例化示例:

// a.cpp
template int Add<int>(const int&, const int&);
template double Add<double>(const double&, const double&);

但这种方式灵活性差,实际项目中一般使用第一种方法。


以下是个人学习时的源代码,没有经过系统整理,谨慎观看!
不过这些源码确实体现了我的思考思路,没有文章内容那么偏向理论,因此我选择贴出来,但是没有作为独立章节展示,大家可以自由选择是否查看~
没有什么高深的内容,完全可以忽略

// Func.h
#pragma once
template<class T>
void FuncT(const T& x)
{
	cout << "void FuncT(const T& x)" << endl;
}

void FuncF();


// Func.cpp
#include<iostream>
using namespace std;
#include"Func.h"

template<class T>
void FuncT(const T& x)
{
	cout << "void FuncT(const T& x)" << endl;
}

void FuncF()
{
	cout << "void FuncF()" << endl;
}


// Test.cpp
#include<iostream>
using namespace std;
#include"Func.h"
void test1()
{
	FuncT(1);
	FuncT(1.1);
	FuncF();

	Stack<int> st;
	st.Push(1);
}

/*
* 编译  -》  检查语法,生成汇编代码
* 一个函数调用汇编是
* call 函数地址
* 虽然这里还没得到准确的函数地址,但是可以通过编译检查
* 而真正的函数地址在链接的时候进行检查
*
* 汇编 -》 生成二进制机器码
* Func.o Test.o
*
* 链接 -》 合并链接,生成可执行程序
* a.out
* (此处链接了各个cpp文件,因此上面的call得到了函数地址)
*
* 那么为什么模板函数分离与定义分离会发生链接错误?
* 说明链接时没填上地址(前面过程都一样)
*
* 对于普通函数而言,它会被编译为指令
* 但是模板函数在没有实例化时,不知道里面的真实类型,无法被编译为指令
* (虽然在test.cpp中实例化了,但是各个.cpp文件在链接之前是不会有交互的)
*/

/*
* 改回声明与定义在同一个文件中后,
* include"Func.h"相当于在这里拷贝了头文件里的内容,
* 此时 FuncT 就不需要链接了,因为本cpp文件就有定义,
* 编译的时候直接就完成了实例化,生成了汇编代码,
* 汇编时直接就获得了汇编指令,获得了地址
*
* 相当于哪里包含了头文件,哪里就有定义,
* 哪里有定义,哪里就不需要链接,
* 调用的对方就有定义,不存在链接问题
* 
* 链接不上的原因是func.cpp那边没有实例化模板,链接找不到地址
*/

/*
* 显示实例化太麻烦,实践中没人用
* 使用模板的地方直接包含了定义,
* 编译时实例化生成了函数的地址,不需要模板
*/

int main()
{
	test1();
	return 0;
}

四、模板优缺点总结

优点 缺点
模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生 模板会导致代码膨胀问题,也会导致编译时间变长
增强了代码的灵活性 出现模板编译错误时,错误信息非常凌乱,不易定位错误

总结

C++ 模板让我们实现了 “类型参数化编程”,是泛型思想的核心。
掌握非类型参数、特化与分离编译,不仅能写出高效的模板代码,还能更深入理解 STL 的设计原理。

💬 学习建议:

  • 尝试自己实现一个简化版的 vectorLess 比较器。
  • 使用偏特化练习不同类型匹配模式。
  • 研究 STL 源码中 std::allocatorstd::iterator_traits 的模板实现。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐