前言

相信学过 C++ 的朋友都接触过模板。入门阶段我们一般就写个 template<class T> T Add(T a, T b) 这种最基础的函数模板,感觉"就是那种感觉"。但当你真的在项目里用起来,或者读到一些库的源码时,会发现模板远比你想象的复杂——非类型参数、全特化、偏特化、指针比较的坑……这些东西不亲自踩一遍,光看理论是很难真正理解的。


一、温习一下:最简单的函数模板

先看 template_pro.h

#pragma once
#include<iostream>
using namespace std;

template<class T>
T Add(const T& left, const T& right)
{
    cout << "T Add(const T& left, const T& right)" << endl;
    return left + right;
}

这就是最基础的函数模板。编译器会根据你调用时传入的实参类型,自动推导 T 是什么,然后帮你生成一份对应的函数。比如:

Add(1, 2);     // T 推导为 int
Add(1.0, 2.0); // T 推导为 double

到此为止都没啥问题,小学生都会。但接下来事情就逐渐离谱起来了。


二、非类型模板参数——模板参数不一定是类型

我们平时写模板,<> 里面放的要么是 class T 要么是 typename T,都是一个类型。但其实模板参数还可以是,这就是"非类型模板参数"。

template_pro.cpp 里的这段:

template<size_t N>
class stack
{
private:
    int _arr[N];
    int _top;
};

这里的 N 不是类型,而是一个 size_t 类型的值。你在实例化的时候要这样写:

stack<5>  st3;   // 栈大小 5
stack<10> st4;   // 栈大小 10

也就是说,stack<5>stack<10> 是两个完全不同的类型,编译器会分别为它们生成两份类代码。

几个重要的点(:

  1. C++20 之前,非类型模板参数只支持整型intsize_tbool 等),double 这种浮点型是不行的

  2. Pasted image 20260723194749.png

  3. C++20 之前,类模板不能省略 <>。你没法写 stack st1;,必须写成 stack<5> st1;。C++20 引入了 CTAD(类模板实参推导)之后才放宽了限制
    Pasted image 20260723195020.png

  4. 这和 array / vector 的区别也值得提一嘴:

array<int, 10> a1;       // 在栈上,大小编译期确定
vector<int> v(10, 2);    // 在堆上,大小运行期确定

array 就是用非类型模板参数实现的,大小是编译期常量,数据存在栈上;vector 是运行期动态分配,数据在堆上。这也解释了为什么 sizeof(array)sizeof(vector) 差那么多。


三、函数模板的特化——指针比较的坑,恶心但得会

看这段场景:

我们定义了一个 Date 类,重载了 < 运算符,然后写了一个通用的 Less 函数模板用来比较两个东西的大小:

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

对于 intDate 对象,这都没问题:

Less(1, 2);          // OK,比较值
Less(d1, d2);        // OK,调用 Date::operator<

但当你传入指针的时候,问题就来了:

Date* p1 = &d1;
Date* p2 = &d2;
Less(p1, p2);        // 比较的是指针的地址,而不是 Date 对象本身!

因为 T 被推导为 Date*left < right 就变成了比较两个指针的地址值,而不是比较它们指向的对象。这在语法上完全正确,逻辑上却大错特错。

怎么解决呢?函数模板特化。我们专门为 Date* 写一份特化版本:

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

这里有一个很容易搞混的细节——const 的位置:

  • const Date*& leftconst 修饰的是 Date,意思是"指向常量 Date 的指针的引用"
  • Date* const& leftconst 修饰的是指针本身,意思是"指针常量的引用"

我们需要的特化是 Date* const&,因为原始模板的参数是 const T&,当 T = Date* 时,const T& 就等价于 Date* const&

不过说实话,与其写一堆特化,很多时候直接写一个普通的重载函数反而更省事:

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

编译器会优先匹配普通函数(非模板),所以这种写法在实践中更常见。但理解特化的原理还是有必要的,因为类模板那里特化是刚需。


四、类模板的全特化与偏特化——这才是重头戏

如果说函数模板的特化是"可选"的,那类模板的特化就是"必备"技能。很多 STL 组件(比如 std::hashstd::less)就是靠特化来针对不同类型做不同处理的。

4.1 基础模板

先定义一个通用的 Data 类模板:

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

4.2 全特化

全特化就是把模板参数全部指定死

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

当你写 Data<int, char> d; 时,编译器会找到这个全特化版本,而不是用通用模板。这很适合"针对某个特定组合做特殊优化"的场景。

4.3 偏特化——部分限制 + 指针/引用特化

偏特化就是不把参数全部定死,而是限制部分参数限制参数的形式

① 限制第二个参数为 double

template<class T1>
class Data<T1, double>   // 只要第二个参数是 double
{
public:
    Data() { cout << "Data<T1, double>" << endl; }
};

Data<float, double>Data<char, double> 都会命中这个版本。

② 限制参数为指针:

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

Data<char*, char*>Data<int*, char*> 都会走这里。这在写容器或者智能指针相关代码时非常有用——你可以针对指针类型做不同的内存管理策略。

③ 限制参数为引用:

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

4.4 匹配优先级

测试一下:

Data<int, int>    d1;   // → Data<T1, T2>         (通用模板)
Data<int, char>   d2;   // → Data<int, char>      (全特化)
Data<float, double> d3; // → Data<T1, double>     (偏特化,第二个是 double)
Data<char*, char*> d6;  // → Data<T1*, T2*>       (偏特化,两个都是指针)
Data<char&, char&> d8;  // → Data<T1&, T2&>       (偏特化,两个都是引用)
Data<char*, char&> d10; // → Data<T1*, T2&>       (偏特化,指针+引用)

匹配规则总结成一句话就是:全特化 > 偏特化 > 通用模板,多个偏特化都匹配时,选最"特殊"的那个。


五、显式实例化

最后看 template_p.cpp 里注释掉的这段:

// 显示实例化
 template
 int Add(const int& left, const int& right);

这就是显式实例化。正常情况下,模板的代码是在你第一次使用的时候才由编译器生成的。但有时候你想提前生成——比如你写了一个模板库,想把实例化提前做好,减少用户的编译时间,或者你想把模板的实现放到 .cpp 文件里(通常模板是必须放在头文件的)。

语法就是在 template 关键字后面写上具体类型:

template int Add(const int& left, const int& right);

这行代码告诉编译器:"别等我调用了,现在就给我生成一份 T=intAdd 函数"。

显式实例化在实际项目里用得不算特别多,但在大型项目或库开发中是优化编译速度的重要手段,了解一下没坏处。


知识点核心要点
函数模板编译器根据实参自动推导类型参数,生成对应函数
非类型模板参数模板参数可以是值(整型),stack<5>stack<10> 是不同类型
函数模板特化解决指针比较等"通用逻辑不适用"的场景,注意 const 的位置
类模板全特化把所有模板参数指定死,适合特定组合的优化
类模板偏特化限制部分参数或参数形式(指针/引用),匹配规则:全特化 > 偏特化 > 通用
显式实例化提前让编译器生成特定类型的模板代码,优化编译速度

模板是 C++ 的灵魂,但也是劝退无数人的深坑。希望这篇文章能帮你少踩几个坑,至少下次看到 template<> 的时候不会一脸懵。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐