C++模板与泛型编程

youncyb 发布于 2025-02-17 200 次阅读 C++


1. 泛型编程的核心思想

1.1 泛型编程的动机与优势

假设我们想要实现一个 swap 函数,分别交换 int, float, std:: string 等不同类型的数据。 在没有泛型编程的情况下,我们可能需要为每种类型都编写一个 swap 函数,代码高度重复且难以维护。

泛型编程核心作用实现了代码复用,但其优势不仅仅止于此,其仍包括以下优势:

  • 代码复用:编写一次代码,适用于多种数据类型。

  • 类型安全:编译时类型检查,避免运行时类型错误。

  • 性能优势:通常比运行时多态更高效 (静态分发)。

  • 与面向对象编程 (OOP) 的对比:编译时多态 vs. 运行时多态。

1.2 C++11 中泛型编程的关键工具:模板

  • 函数模板和类模板是 C++11 实现泛型编程的主要手段。
  • 模板的工作原理:代码生成器,根据不同的类型参数,由编译器在编译时生成具体的代码,所以 C++的模板编程,更像是一门指导编译器如何生成代码的语言。

2. 函数模板

2.1 函数模板基础语法

模板定义如下:

template <typename T> // 旧式定义方式:typename 可以替换为<class T>
返回类型 函数名(参数列表) {
    // 函数体,可以使用类型参数 T
}

例如一个实际的 compare 函数定义,其可以接受各种类型的实参,只需要该类型实参支持 < 操作符。

template <typename T>
int compare(const T &a, const T &b)
{
    if (a < b) return -1;
    if (b < a) return 1;
    return 0;
}

2.2 函数模板的实例化

2.2.1 隐式实例化

隐式实例化是指 编译器在需要的时候自动为你生成模板的特定版本代码 的过程,而无需你显式地指示编译器去做这件事。简单来说,就是 用到了才生成,自动生成

隐式实例化通常发生在以下两种情况:

  • 函数模板的调用: 当你调用一个函数模板,并且编译器能够 根据函数调用的参数类型推导出模板参数 时,就会发生隐式实例化。
  • 类模板的使用 (创建对象、访问成员等): 当你 声明一个类模板类型的对象,或者 访问类模板对象的成员 时,编译器会根据你指定的类型参数进行隐式实例化。

过程:

  1. 函数调用: 你在代码中调用了一个函数模板,例如 compare(a, b)。
  2. 类型推导: 编译器检查函数调用的参数 a 和 b 的类型。假设 a 和 b 都是 int 类型。
  3. 实例化: 编译器发现需要调用 compare 函数模板,并且参数类型是 int。 如果编译器还没有为 compare <int> 生成过代码,它就会 隐式地实例化 一个针对 int 类型的 compare 函数,也就是生成 int compare <int>(const int &a, const int &b) 的具体函数代码。
  4. 代码使用: 编译器使用新生成的 compare <int> 函数代码来完成你的函数调用。
#include <iostream>

template <typename T>
int compare(const T &a, const T &b) {
    std::cout << "函数模板 compare<T> 被实例化,T 的类型是 " << typeid(T).name() << std::endl;
    if (a < b) return -1;
    if (b < a) return 1;
    return 0;
}

int main() {
    int int_a = 10, int_b = 20;
    double double_a = 3.14, double_b = 2.71;

    int compare_int = compare(int_a, int_b);    // 隐式实例化 compare<int>
    double compare_double = compare(double_a, double_b); // 隐式实例化 compare<double>

    std::cout << "compare int: " << compare_int << std::endl;
    std::cout << "compare double: " << compare_double << std::endl;

    return 0;
}

// 函数模板 compare<T> 被实例化,T 的类型是 i
// 函数模板 compare<T> 被实例化,T 的类型是 d
// compare int: -1
// compare double: 1

在相同的文件中,当生成了 int 版本的 compare 后,后续再遇到 int 版本的 compare 则会使用之前生成的实例。

2.2.2 显示实例化

在大系统中,多个文件实例化相同的代码,会导致代码体积膨胀,符号重复等问题。为了解决这些问题,我们可以使用显示实例化。

显式实例化是一种 程序员主动告知编译器,强制其为指定的模板参数类型生成函数模板实例 的机制。 与隐式实例化的“按需生成”不同,显式实例化是 强制编译器立即生成 特定版本的函数代码,而 不依赖于函数是否被实际调用

显示实例化定义如下,通常在文件中指定 extern 关键字,表示在其他位置生成了该实例。

// a.cpp
extern template declaration; // extern 声明该模板,表示其他位置生成了一个该模板实例

// b.cpp
template declaration; // 对该模板的显示实例化

例如:

// a.cpp
extern template int compare<int>(const int &a, const int &b); // 声明compare<int>,不在本文件实例化
int result = compare(a, b);

// 在b.cpp中进行显式实例化
template <typename T>
int compare(const T &a, const T &b) {
    std::cout << "函数模板 compare<T> 被实例化,T 的类型是 " << typeid(T).name() << std::endl;
    if (a < b) return -1;
    if (b < a) return 1;
    return 0;
}
template int compare<int>(const int&, const int&); // 实例化int版本的compare

2.3 模板参数

2.3.1 模板参数作用域

模板参数同函数参数类似,会覆盖外围相同命名的参数,并且在模板内部不能重用模板参数。例如:

using A = double;
template <typename A, typename B> void func(A a, B b)
{
    A tmp = a; // tmp的类型是A,而非double
    double B; // 错误,B已经是模板参数,无法重用
}

2.3.2 默认模板参数

模板参数也支持设置默认参数:

template <typename T = int>
void print(T val = 0) { /*...*/ }
print();  // T 推断为 int

2.3.3 非模板参数

我们也可以使用非模板类型的参数:

#include <iostream>

template <typename T, unsigned N, unsigned M>
void func(const T (&p1)[N], const T (&p2)[M])
{
    std::cout << "first of p1 is " << p1[0] << ", first of p2 is " << p2[0] << '\n';
}

int main() {
    int arr1[] = {1, 2, 3};
    int arr2[] = {4, 5, 6, 7};
    func(arr1, arr2); // 调用 func 函数模板,T 推断为 int, N 推断为 3, M 推断为 4

    double arr3[] = {1.1, 2.2};
    double arr4[] = {3.3, 4.4, 5.5};
    func(arr3, arr4); // 调用 func 函数模板,T 推断为 double, N 推断为 2, M 推断为 3

    char arr5[] = {'a', 'b', 'c'};
    char arr6[] = {'d', 'e'};
    func(arr5, arr6); // 调用 func 函数模板,T 推断为 char, N 推断为 3, M 推断为 2

    return 0;
}

2.3.4 可变参数模板

C++ 的可变参数模板是 C++11 引入的特性,允许模板接受任意数量和类型的参数。其基础语法如下所示:

template <typename T, typename... Args>
void func(const T &t, const Args&... rest) {...}

为了获取模板参数的数量和函数形参的数量,我们可以使用 sizeof...() 函数。

#include <iostream>
#include <typeinfo>
#include <vector>

template <typename T, typename... Args>
void func(const T& t, const Args&... rest) {
    // 输出第一个参数信息
    std::cout << "[Base Argument]\n"
              << "Type: " << typeid(T).name() 
              << "\nValue: " << t 
              << "\n\n";

    // 输出参数包信息
    std::cout << "[Variadic Arguments]\n"
              << "Template parameters: " << sizeof...(Args) 
              << "\nFunction parameters: " << sizeof...(rest) 
              << "\n\n";
}

int main() {
    // 示例 1: 混合类型参数
    func(42, 3.14, "hello", true, 'A');

    // 示例 2: 空参数包
    func(100);  // rest 参数包为空

    // 示例 3: 容器参数
    std::string s = "world";
    func(s, std::vector<int>{1, 2, 3}, 5.6f);
}

[Base Argument]
Type: i
Value: 42

[Variadic Arguments]
Template parameters: 4
Function parameters: 4
-----------------------------------------------
[Base Argument]
Type: i
Value: 100

[Variadic Arguments]
Template parameters: 0
Function parameters: 0
-----------------------------------------------
[Base Argument]
Type: NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
Value: world

[Variadic Arguments]
Template parameters: 2
Function parameters: 2

2.4 模板实参推导

允许编译器根据函数调用时提供的实参类型,自动推导出 函数模板或类模板实例化的模板实参。这项特性极大地简化了模板的使用,使得我们无需在每次调用模板函数或创建模板类对象时都显式地指定模板实参,从而提高了代码的简洁性和可读性。

2.4.1 基本类型匹配

模板函数存在多个参数,且使用了同一个模板参数,则必须保证类型一致,模板实参不会发生隐式的类型转换。例如:

template <typename T> int compare(T a, T b) {...}
int result = compare(1, 3.14); // 错误,a的类型是int,b的类型是double,类型不一致

虽然无法使用隐式转换,但可以通过 显式模板参数,进行强制转换:

int result = compare<double>(1, 3.14); // 正确,a的类型是double,b的类型double

或者使用两个模板参数:

template <typename T, typename U> int compare(T a, U b) {...}
int result = compare(1, 3.14); // 正确,a的类型是int,b的类型是double

除了以上规则外,模板参数会自动忽略顶层 const。并且支持以下两条规则:

  1. 非 const 对象的引用(指针)可以传递给 const 的引用(指针)形参

    template <typename T> void func1(T a, T b) {...}
    template <typename T> void func2(const T &a, const T &b) {...}
    
    std::string str1("Hello");
    const std::string str2("World");
    func1(str1, str2); // 正确,func1(std::string, std::string),str2的const被忽略
    
    func2(str1, str2); // 正确,func2(const std::string&, const std::string&), str1被转换为const
    
  2. 函数的形参非引用类型,则可以将数组和函数类型的实参,转换为指针类型

    int a[10], b[42];
    func1(a, b); // 正确,func1(int *, int *),但丢失了数组的维度信息
    
    func2(a, b); // 错误,func2(int a[10], int b[42]),数组的维度不匹配
    

2.4.2 尾置返回类型

当我们编写接受迭代器类型参数的模板函数,并返回其中的一个元素引用,则必须使用尾置返回方式。

template <typename It>
auto func(It begin, It end) -> decltype(*begin) // 解引用运算符返回一个左值,因此通过decltype推断的类型为元素的类型的引用,假设传入的vector<string>,则为string&
{
    return *begin;
}

int main() {
    std::vector<int> v{10, 20, 30};
    auto& result = func(v.begin(), v.end()); //auto推导时的类型推导规则,特别是引用和顶层的const会被忽略的情况,如若保持引用,则使用auto&
    result ++;
    std::cout << result << " (type: " << typeid(result).name() << ") vector[0] is " << *v.begin(); // vector[0] 被修改为11
}
template <typename It>
auto func(It begin, It end) -> decltype(*begin + 0) // 解引用运算符返回一个左值,其必须支持`+`操作符,得到一个右值,因此通过decltype推断的类型为元素的类型的右值常量
{
    return *begin;
}

int main() {
    std::vector<int> v{10, 20, 30};
    auto result = func(v.begin(), v.end()); //auto推导时的类型推导规则,特别是引用和顶层的const会被忽略的情况,如若保持引用,则使用auto&
    result ++;
    std::cout << result << " (type: " << typeid(result).name() << ") vector[0] is " << *v.begin(); //vector[0] 不变
}

如果我们想返回其中一个元素的拷贝,则必须使用标准库:type_traits

template <typename It>
auto func(It begin, It end) -> 
	typename remove_reference<decltype(*begin)>::type // 假设传递vector<string>,返回元素拷贝,类型:string
{
    return *begin;
}

2.4.3 引用

如果函数参数是左值引用类型,则只能传入左值类型的实参。

template <typename T> void func(T &a) {...}

func(i); // 正确,func(int &),T = int
func(ci); // 正确,func(const int &),T = const int
func(42); // 错误,42是右值

如果函数参数是 const 左值引用类型,则可以传入任意类型的参数:

template <typename T> void func(const T &a) {...}

func(i); // 正确,func(int &),T = int
func(ci); // 正确,func(const int &),T = int
func(42); // 正确,func(const int &),T = int

如果参数是右值引用,按规则我们应该传入一个右值。但是如果我们传入一个左值也是可行的,当右值引用的形参遇到一个左值的实参,例如:形参 T&& 接受一个 int 的左值,则 T 被推断为 int&,而函数原型推断后则是:func(int& &&)

通常我们无法定义一个引用的引用,但其有两个例外:

  1. 类型别名

    int x = 10;
    
    int &ref_x = x; // 使用 typedef 创建引用类型别名 
    typedef int& intRef; 
    intRef ref_ref_x = ref_x; // ref_ref_x 仍然直接绑定到 x 
    
    // 使用 using 创建引用类型别名
    using intRef_using = int&; 
    intRef_using ref_ref_using = ref_x; // ref_ref_using 也直接绑定到 x
    
  2. 模板类型参数

    当遇到模板类型参数时,则会发生 引用折叠

    • X& &X& &&X&& & 被折叠为 X&
    • X&& && 被折叠为 X&&

    所以上文 func(int& &&),实际上折叠为:func(int&)T = int&,当接受一个右值时,T = int

2.4.4 完美转发

完美转发用于将实参类型一丝不差的转发给内部调用的函数,避免在模板编程的过程中,参数类型变化导致函数出现意外的结果。

我们先从一个简单的例子进行了解:

void g(int arg1, int &arg2)
{
    std::cout << "first arg is " << arg1 << ", second arg is " << arg2++ << '\n';
}

template <typename F, typename T, typename U>
void flip1(F func, T a, U b)
{
    func(b, a);
}

调用函数 g 后,会使用参数 b++,但如果我们通过 flip1(g, a, b),则不会导致参数 b 自增。

int a = 10;
flip1(g, a, 42); // a由于是拷贝的方式传入,所以g无法修改变量a

为了使得 g 可以修改变量 a,我们对 flip1 的函数形参进行修改:

template <typename F, typename T, typename U>
void flip1(F func, T &&a, U &&b)
{
    func(b, a);
}
flip1(g, a, 42); // a的传入类型是int& &&,引用折叠后,变为int&,所以g可以修改变量a

但新的问题又出现了,我们将函数 g 修改为接受参数 a 右值引用的形参:

void g(int &&arg1, int &arg2)
{
    std::cout << "first arg is " << arg1 << ", second arg is " << arg2++ << '\n';
}

template <typename F, typename T, typename U>
void flip1(F func, T &&a, U &&b)
{
    func(b, a);
}

a 的传入类型是 int& &&,引用折叠后,变为 int&,b 传入的类型是 int&&。看似 b 的类型是 int && 可以传递给 g 的第一个参数,但这会发生编译错误,关键点在于 函数参数(局部变量)是一个左值表达式,即使 b 的类型是 int &&,仍然被当做一个左值。所以会报错。

flip1(g, a, 42); 

解决方案:使用 std::forward<T>

#include <iostream>
#include <utility> // 需要包含此头文件以使用 std::forward

template<typename F, typename T, typename U>
void flip2(F func, T &&a, U &&b) {
  std::cout << "flip2" << '\n';
  func(std::forward<T>(b), std::forward<U>(a)); // 使用 std::forward 完美转发
}

void g(int &&arg1, int &arg2) {
  std::cout << "first arg is " << arg1 << ", second arg is " << arg2++ << '\n';
}

int main() {
  int a = 10;
  flip2(g, a, 42);
}

3. 类模板

3.1 类模板基础语法

类模板定义如下:

  • 在使用成员类型,例如 size_type,必须以 typename 开头,表示:: size_type 是成员属性,而不是一个静态定义
  • 定义在外部的成员,必须使用模板格式: template <typename T> 开头
template <typename T>
class Blob
{
public:
    typedef typename std::vector<T>::size_type size_type; // 必须使用typename,表示::size_type是成员属性,而不是一个静态定义
    Blob();
    Blob(std::initializer_list<T> il);
    size_type size() const {return data->size();}
    T& operator[](size_type i);
private:
    std::shared_ptr<std::vector<T>> data;
    void check(size_type i, const std::string &msg) const;
};
template <typename T> // 定义在外部的成员,必须使用模板格式: template <typename T> 开头
void Blob<T>::check(size_type i, const std::string &msg) const
{
    if(i >= data->size())
    {
        throw(std::out_of_range(msg));
    }
}

template <typename T>
T& Blob<T>::operator[](size_type i)
{
    check(i, "index out of range");
    return (*data)[i];
}

3.2 类模板的实例化

不同于函数模板可以自动推导类型,类模板在实例化时必须显式的指定类型:

Blob<int> blob1;
Blob<int> blob2 = {0, 1, 2, 3, 4};

类模板形成的每个模板实例,都是一个独立的类。Blob<std::string>Blob<int> 没有任何关联。对于类的静态成员也是如此,每个实例化后的类独享一个静态成员。

// 方式一:
auto ct = Foo<int>::count(); // 访问类的静态成员

// 方式二:
Foo<int> fi;
auto ct = fi.count(); 

默认情况下,一个类模板的成员函数,只有在被使用时,才会模板实例化,没有使用的成员函数则不会进行模板实例化。

Blob<int> squares = {0, 2, 4, 6}; // 实例化Blob<int>和构造函数Blob<int>::Blob(std::initializer_list<int>)
for(size_t i = 0; i < squares.size(); ++i) // 实例化 Blob<int>::size()
{
    squares[i] = i * i; // 实例化Blob<int>::operator[](size_type i)
}

3.3 成员模板

类模板本身已经提供了类型泛化的能力,但有时我们需要更精细的控制,让类模板的某些成员函数或成员类能够独立于类模板的类型参数,处理更多不同的类型。 成员模板主要用于以下场景:

  1. 更灵活的类型操作: 允许成员函数处理与类模板类型参数无关的其他类型。例如,一个类模板可能用于存储某种类型的数据,但我们可能希望提供一个成员函数,可以接受并处理其他类型的数据,或者进行类型转换等操作。
  2. 泛型编程的扩展: 在类模板内部实现更通用的算法或数据结构,这些算法或数据结构可能需要处理多种类型,而不仅仅是类模板自身的类型。
  3. 适配器模式: 成员模板可以作为适配器,将不同类型的对象转换为类模板可以处理的类型,或者反之。
  4. 实现更通用的构造、赋值和转换操作: 例如,实现一个更通用的拷贝构造函数或赋值运算符,可以接受并处理不同类型的对象。
template <typename T> // 类模板参数列表
class MyTemplateClass {
public:
    // ... 类模板的其他成员 ...

    template <typename U> // 成员函数模板参数列表
    void memberFunctionTemplate(U arg) {
        // ... 使用 T (类模板参数) 和 U (成员函数模板参数) 的代码 ...
    }
};

4. 模板特例化

模板特例化是一种为特定类型提供模板的不同实现的机制。 当我们定义一个模板(函数模板或类模板)时,我们创建了一个通用代码蓝图,它可以用于多种类型。 然而,在某些情况下,通用的模板实现可能对于某些特定类型不是最优的,甚至无法工作。 这时,我们就需要使用模板特例化来为这些特定类型提供定制化的实现。

模板特例化主要解决以下问题:

  1. 性能优化: 通用的模板实现可能在某些特定类型上效率不高。例如,对于 std::vector<bool>,标准库提供了特例化版本,使用位压缩技术来节省内存空间。
  2. 功能定制: 某些类型可能需要模板提供不同的行为或功能。例如,一个通用的排序算法模板可能需要为字符串类型提供字典序排序,而不是默认的比较操作。
  3. 解决编译错误: 通用的模板实现可能对于某些特定类型无法编译通过。例如,模板代码可能使用某些类型不支持的操作,这时就需要为这些类型提供特例化版本,避免编译错误。
  4. 处理边界情况或特殊情况: 某些类型可能是模板逻辑中的边界情况或特殊情况,需要单独处理。例如,当模板参数是指针类型时,可能需要进行空指针检查或特殊处理。

当编译器遇到模板实例化时,它会按照以下顺序查找最匹配的模板:

  1. 完全匹配的非模板函数或类 (如果存在,优先选择非模板)
  2. 完全特例化版本 (如果存在,优先选择完全特例化)
  3. 部分特例化版本 (如果存在,选择最匹配的部分特例化版本)
  4. 通用模板版本 (如果以上都没有找到,则使用通用模板)

4.1 函数模板特例化

函数模板特例化定义如下:

template <> // 特例化声明时,模板参数列表为空
return_type function_name<specific_type_arguments>(parameters);
{
    // 特例化的函数实现代码
}

例如,我们想为 std::string 类型的参数提供移动语义的交换:

  • 特例化声明时,template <> 后面是空的模板参数列表,表示这是一个特例化版本,而不是新的模板。
  • 在特例化函数名后面,需要显式地指定特例化类型参数,例如 swap_values<std::string >。
  • 特例化版本需要提供完整的函数定义,包括函数体。
#include <iostream>
#include <string>
#include <utility> // std::move

// 通用交换函数模板
template <typename T>
void swap_values(T& a, T& b) {
    std::cout << "Generic swap called for type: " << typeid(T).name() << std::endl;
    T temp = a;
    a = b;
    b = temp;
}

// 函数模板特例化,针对 std::string 类型
template <> // 模板参数列表为空
void swap_values<std::string>(std::string& a, std::string& b) {
    std::cout << "Specialized swap for std::string called!" << std::endl;
    using std::swap; // 允许 ADL 查找 std::swap,以便调用 std::string 的 swap 方法
    swap(a, b); // 使用 std::string 的高效 swap 方法 (通常基于移动语义)
}

int main() {
    int x = 10, y = 20;
    swap_values(x, y); // 调用通用版本,T 被推导为 int

    std::string str1 = "hello", str2 = "world";
    swap_values(str1, str2); // 调用特例化版本,因为类型是 std::string

    return 0;
}

4.2 类模板特例化

类模板特例化定义如下:

template <> // 特例化声明时,模板参数列表为空
class class_name<specific_type_arguments> {
    // 特例化的类定义 (成员变量、成员函数等)
    // 可以与通用模板完全不同
};
  • 类模板特例化可以完全改变类的内部结构和成员。Array<bool, 0> 特例化版本内部使用了 std::vector<bool>,而不是固定的数组 data[Size]
  • 特例化版本可以有不同的构造函数、成员函数和数据成员。
#include <iostream>
#include <vector>
#include <stdexcept>

// 通用 Array 类模板
template <typename T, size_t Size>
class Array {
private:
    T data[Size];
public:
    Array() = default;

    T& operator[](size_t index) {
        if (index >= Size) {
            throw std::out_of_range("Index out of range");
        }
        return data[index];
    }

    const T& operator[](size_t index) const {
        if (index >= Size) {
            throw std::out_of_range("Index out of range");
        }
        return data[index];
    }
    // ... 其他成员函数 ...
};

// 类模板特例化,针对 bool 类型
template <>
class Array<bool, 0> { // 注意:这里 size 设为 0 只是为了演示特例化,实际应用中 size 应该根据需求设计
private:
    std::vector<bool> data; // 使用 std::vector<bool> 来实现位压缩
public:
    Array() = default;
    Array(size_t size) : data(size) {} // 添加构造函数

    bool& operator[](size_t index) {
        return data[index];
    }

    const bool& operator[](size_t index) const {
        return data[index];
    }

    size_t size() const {
        return data.size();
    }
    // ... 其他成员函数 ...
};

int main() {
    Array<int, 5> intArray; // 使用通用版本
    intArray[0] = 10;
    std::cout << "intArray[0]: " << intArray[0] << std::endl;

    Array<bool, 0> boolArray(10); // 使用特例化版本 (注意 size 参数)
    boolArray[0] = true;
    std::cout << "boolArray[0]: " << boolArray[0] << std::endl;
    std::cout << "boolArray size: " << boolArray.size() << std::endl;

    return 0;
}

4.3 类模板某些成员函数的特例化

我们可以只特例化类模板的某些成员函数,而不是整个类。 这在只需要为特定类型定制某个成员函数的行为时非常有用,而其他成员函数仍然可以使用通用模板的实现。

template <typename T> // 原始类模板的参数列表
class class_name {
    // ... 类模板的通用定义 ...

    template <> // 成员函数特例化声明时,模板参数列表为空
    return_type member_function_name<specific_type_arguments>(parameters); // 需要指定特例化的类型参数
};

// 特例化成员函数需要在类外部定义
template <typename T> // 原始类模板的参数列表
template <> // 成员函数特例化声明时,模板参数列表为空
return_type class_name<T>::member_function_name<specific_type_arguments>(parameters) // 需要指定类模板的类型参数和成员函数特例化的类型参数
{
    // 特例化的成员函数实现代码
}
  • 成员函数特例化需要在类内部进行声明,然后在类外部进行定义。
  • 特例化成员函数时,仍然需要指定类模板的类型参数 (Container<T>)<std::string>)
  • 调用特例化成员函数时,需要显式地指定特例化的类型参数,例如 stringContainer.print<std::string>()
#include <iostream>
#include <vector>
#include <string>

template <typename T>
class Container {
private:
    std::vector<T> data;
public:
    Container() = default;

    void add(const T& item) {
        data.push_back(item);
    }

    // 通用的打印函数模板
    void print() const {
        std::cout << "Generic print: [ ";
        for (const auto& item : data) {
            std::cout << item << " ";
        }
        std::cout << "]" << std::endl;
    }

    // 成员函数特例化声明 (在类内部声明)
    template <>
    void print<std::string>() const; // 只需要声明,不需要定义在类内部
};

// 成员函数特例化定义 (在类外部定义)
template <typename T> // 原始类模板的参数列表
template <> // 成员函数特例化声明时,模板参数列表为空
void Container<T>::print<std::string>() const { // 注意语法: Container<T>::print<std::string>()
    std::cout << "Specialized string print: { ";
    for (const auto& item : data) {
        std::cout << "\"" << item << "\" "; // 特殊格式化字符串
    }
    std::cout << "}" << std::endl;
}


int main() {
    Container<int> intContainer;
    intContainer.add(1);
    intContainer.add(2);
    intContainer.print(); // 调用通用 print 版本

    Container<std::string> stringContainer;
    stringContainer.add("hello");
    stringContainer.add("world");
    stringContainer.print<std::string>(); // 调用特例化 print<std::string> 版本 (注意显式指定)

    return 0;
}

4.4 类模板的部分特例化

类模板的部分特例化允许我们为 类模板的部分类型参数 提供特例化实现,而其他类型参数仍然保持通用。 函数模板 不支持 部分特例化。

语法定义如下:

template <template_parameter_list> // 部分特例化声明时,模板参数列表可以不为空,但要比原始模板的参数列表少或部分特例化
class class_name<specialized_type_arguments, remaining_template_parameters> {
    // 部分特例化的类定义
};
  • 部分特例化仍然是模板,它有自己的模板参数列表 (template <typename U, typename Allocator>),但这个列表可能比原始模板的参数列表少或部分特例化。
  • 在部分特例化声明中,我们指定了部分类型参数的特殊形式 (Storage<U*, Allocator>),这里第一个类型参数 T 被特例化为指针类型 U*,而第二个类型参数 Allocator 仍然保持通用。
  • 当编译器遇到 Storage<int*, std::allocator<int*>> 时,会优先选择部分特例化版本,因为它更符合类型匹配。
#include <iostream>

// 通用 Storage 类模板
template <typename T, typename Allocator>
class Storage {
public:
    Storage() {
        std::cout << "Generic Storage for type: " << typeid(T).name() << std::endl;
    }
    void allocate() { std::cout << "Generic allocate" << std::endl; }
    void deallocate() { std::cout << "Generic deallocate" << std::endl; }
    // ... 通用 Storage 实现 ...
};

// 类模板部分特例化,针对指针类型 T*
template <typename U, typename Allocator> // 部分特例化声明时,模板参数列表可以不为空,这里 U 对应指针指向的类型
class Storage<U*, Allocator> { // 注意:第一个模板参数是 U*,表示 T 被特例化为指针类型
public:
    Storage() {
        std::cout << "Partial specialization for pointer type: " << typeid(U*).name() << std::endl;
    }
    void allocate() { std::cout << "Specialized allocate for pointer" << std::endl; }
    void deallocate() { std::cout << "Specialized deallocate for pointer" << std::endl; }
    // ... 指针类型的特殊 Storage 实现 ...
};


int main() {
    Storage<int, std::allocator<int>> intStorage; // 使用通用版本
    intStorage.allocate();

    Storage<int*, std::allocator<int*>> pointerStorage; // 使用部分特例化版本,因为 T 是 int*
    pointerStorage.allocate();

    return 0;
}