目录

读 STL、Boost 这类库时,满眼都是 template,不搞懂泛化、特化和实例化,很容易卡在源码门口。这篇文章咱们把三件事捋清楚:它们各是啥、编译器怎么选版本、顺带用一个”类型是否相同”的小例子串起来。


一、泛化:先有一张"万能图纸"

泛化模板就是最普通的那种模板:用类型参数(或非类型参数)占个位,表示”以后再说啥类型”,代码先按通用逻辑写。

1. 函数模板

一组函数逻辑一样,只是操作的类型不同,就用函数模板兜住。

// T 是占位:具体啥类型,调用时再定
template <typename T>
void swap(T& a, T& b)
{
    T temp = a;
    a = b;
    b = temp;
}
  • T 可以是 int、double、std::string 等,都行。
  • 注意:模板本身不直接变成机器码,它只是给编译器的一张”生成代码的图纸”。

2. 类模板

类里用到的类型也想抽出来,就用类模板。下面这个迷你容器把数据成员收成 _data,命名上更贴近常见写法。

template <typename T>
class Container {
private:
    T _data;
public:
    explicit Container(T d) : _data(d) {}
    T get() const { return _data; }
};
  • 成员函数可以全写在类里;要拆到类外,记得前面带上 template 那一套。

二、特化:图纸不够用,再开一张"专款"

泛化版本对某些类型不合适,或者你想针对性优化,就给这些类型单独写一版,这就是特化。分全特化和偏特化两种。

1. 全特化:参数全钉死

所有模板参数都换成具体类型,把泛化那一套整段换掉。

函数模板全特化示例:对 const char* 单独打招呼。

template <typename T>
void print(T value)
{
    std::cout << "Generic: " << value << std::endl;
}

template <>
void print<const char*>(const char* value)
{
    std::cout << "Specialized for string: " << value << std::endl;
}

传 “hello” 这种会落到 const char*,优先走特化;别的类型照旧走泛化。

类模板全特化示例:给 int 单独一版 Printer。

template <typename T>
class Printer {
public:
    void print(T value)
    {
        std::cout << "Generic class: " << value << std::endl;
    }
};

template <>
class Printer<int> {
public:
    void print(int value)
    {
        std::cout << "Specialized class for int: " << value << std::endl;
    }
};

2. 偏特化:只钉死一部分(或改成指针形态)

多参数模板里,可以只固定其中几个参数,或者专门给”两个都是指针”这种形态写一版。函数模板不能做偏特化,只有类模板可以,这点别记反了。

template <typename T, typename U>
class Pair {
public:
    Pair(T a, U b) : _first(a), _second(b) {}
    void show()
    {
        std::cout << "Generic: " << _first << ", " << _second << std::endl;
    }
private:
    T _first;
    U _second;
};

// 偏特化:第一个参数固定成 int
template <typename U>
class Pair<int, U> {
public:
    Pair(int a, U b) : _first(a), _second(b) {}
    void show()
    {
        std::cout << "Partial (T=int): " << _first << ", " << _second << std::endl;
    }
private:
    int _first;
    U _second;
};

// 偏特化:两个都是指针
template <typename T, typename U>
class Pair<T*, U*> {
public:
    Pair(T* a, U* b) : _first(a), _second(b) {}
    void show()
    {
        std::cout << "Partial (pointers): " << *_first << ", " << *_second << std::endl;
    }
private:
    T* _first;
    U* _second;
};

三、实例化:图纸终于变成真代码

实例化就是编译器拿着模板(和可能的特化),按你用的具体类型生成出一份真正的函数或类。模板自己不长肉,实例化之后才落地。

1. 隐式实例化:你写调用,编译器生成

template <typename T>
T add(T a, T b) { return a + b; }

int main()
{
    int x = add(1, 2);          // 生成 add<int>
    double y = add(1.5, 2.5);   // 生成 add<double>
    return 0;
}

int 一版、double 一版,都是编译器悄悄帮你实例化出来的。

2. 显式实例化:你点名要哪一版

自己写 template …,等于告诉编译器:“这一版在这个翻译单元里给我生成好。“分离编译、控制符号时常用。

template int add<int>(int, int);
template double add<double>(double, double);
template class Container<int>;

实务上很多人把显式实例化放在 .cpp 里,和声明分开,省得链接时重复定义闹心。


四、匹配优先级:编译器先找谁?

可以粗略记一句:特化比泛化”更具体”就优先,全特化、偏特化都比光秃秃的泛化更贴类型;具体规则以标准为准,写库时记住”越特化越像定制款”就行。

三者关系可以一句话说完:泛化是通用图纸,特化是某几类类型的专款,实例化是”按图纸把代码印出来”的那一步。

场景适合干啥
泛化写通用逻辑,例如容器、算法骨架
特化某类型要特殊行为或要省空间、提速
显式实例化管住在哪个 .cpp 里生成,配合分离编译

五、小例子:用特化做"类型是否相同"

<type_traits> 里那一堆 trait,很多就是泛化 + 偏特化堆出来的。SFINAE(替换失败不是错误)再配合一下,能玩出很多静态判断。这里只演示最朴素的一种:判断两个类型是否相同。

思路很简单:

  • 主模板:默认认为不相同,“值” 为 false。
  • 当两个模板参数其实是同一种类型时,偏特化一版,改成 true。
  • 读静态成员 _value 就知道结果。

下面结构体起名用 TypeEqual,避免和标准库的 std::is_same 撞名;成员按习惯用 _value。

#include <iostream>

template <typename T, typename U>
struct TypeEqual {
    static constexpr bool _value = false;
};

template <typename T>
struct TypeEqual<T, T> {
    static constexpr bool _value = true;
};

template <typename T, typename U>
void check_same_type()
{
    if (TypeEqual<T, U>::_value)
        std::cout << "T 和 U 是相同类型\n";
    else
        std::cout << "T 和 U 是不同类型\n";
}

int main()
{
    check_same_type<int, int>();
    check_same_type<int, double>();
    check_same_type<char, char>();
    check_same_type<int*, int*>();
    return 0;
}

日常项目里直接用 std::is_same 就行;这里主要是让你看清偏特化长啥样。把泛化、特化、实例化三块串起来,读模板库源码时心里会踏实很多。