夜雨聆风学习资料网

ARTICLE · 1115742

C++ 高级编程:类模板深度讲解

C++ 高级编程:类模板深度讲解

引言

在 C++ 中,类模板(class template) 是一种强大的泛型编程工具。它本质上是一种 **"规范描述(prescription)"**——规定了如何创建某个类,而这个类中的一个或多个类型或值被参数化。

对于 C++ 初学者来说,完全可以直接使用标准库中的类模板(如 vector、list、map、shared_ptr 等),而无需了解模板定义背后的机制。事实上,本书前面章节中已经多次使用这些类模板,却没有深入讲解模板机制。只有较高级的 C++ 程序员才会定义自己的类模板,并使用本章所述的机制。因此,本章内容属于 C++ 高级话题。

本章分为 入门 与 高级 两部分:

  • 入门部分:介绍如何定义类模板、简单用法、实例化机制,以及如何为类模板定义成员函数、静态数据成员和嵌套类型。
  • 高级部分:面向生产级应用程序开发,包括编译器如何实例化模板、程序组织要求、特化与部分特化、模板中名字的解析规则、命名空间中的类模板定义,最后以一个较大规模的完整示例收尾。

一、入门部分:定义与使用类模板

1.1 为什么需要类模板

假设我们需要一个可以存储任意类型元素的"栈":

classIntStack {
std::vector<int> data;
public:
voidpush(int x){ data.push_back(x); }
intpop(){ int v = data.back(); data.pop_back(); return v; }
boolempty()const{ return data.empty(); }
};

如果还需要 DoubleStack、StringStack……难道要为每种类型都写一遍?显然不现实。类模板正是为解决这类 "类型参数化" 问题而生。

1.2 类模板的定义

template <typename T>
classStack {
public:
voidpush(const T& x);
T    pop();
boolempty()const;
private:
std::vector<T> data;
};

要点:

  • template <typename T> 是模板参数列表,T 是类型参数(也可写成 class T)。
  • 在类模板内部,T 就像普通类型名一样使用。
  • 类模板不是类,而是"生成类的蓝图"。

1.3 成员函数的定义

成员函数可以在类内定义(隐式 inline),也可以在类外定义。类外定义时,必须带上模板参数列表:

template <typename T>
void Stack<T>::push(const T& x) {
    data.push_back(x);
}

template <typename T>
T Stack<T>::pop() {
    T v = data.back();
    data.pop_back();
return v;
}

template <typename T>
bool Stack<T>::empty() const {
return data.empty();
}

注意 Stack<T>:: 前缀——**每个类外成员函数定义前都必须重新声明 template <typename T>**。

1.4 实例化(Instantiation)

使用类模板时,编译器会根据实参类型实例化出一个具体的类:

Stack<int>         si;   // 实例化 Stack<int>
Stack<std::string> ss;  // 实例化 Stack<std::string>

si.push(42);
ss.push("hello");

Stack<int> 与 Stack<std::string> 是两个完全不同的类型,彼此之间没有任何继承或转换关系。

1.5 类模板的成员类型

类模板可以拥有多种成员:

(1)普通成员函数

如上例中的 push、pop、empty。

(2)静态数据成员

template <typename T>
classCounter {
public:
    Counter()  { ++count; }
    ~Counter() { --count; }
staticint count;
};

template <typename T>
int Counter<T>::count = 0;   // 类外定义静态成员

每个实例化类型 Counter<int>、Counter<double> 各有独立的 count。

(3)嵌套类型

template <typename T>
classContainer {
public:
using value_type = T;
using size_type  = std::size_t;

classiterator {/* ... */ };
};

标准库容器(如 vector)正是通过 value_type 等嵌套 typedef 与算法库协作。

(4)成员模板

类模板内还可以再定义模板成员:

template <typename T>
classWrapper {
public:
template <typename U>
voidassign(const U& u){ value = static_cast<T>(u); }
private:
    T value{};
};

1.6 一个完整的小例子

#include<vector>
#include<stdexcept>

template <typename T>
classStack {
public:
voidpush(const T& x){ data.push_back(x); }
voidpop(){ data.pop_back(); }
T&   top(){ return data.back(); }
boolempty()const{ return data.empty(); }
std::size_tsize()const{ return data.size(); }
private:
std::vector<T> data;
};

intmain(){
    Stack<int> s;
    s.push(1); s.push(2); s.push(3);
while (!s.empty()) {
std::cout << s.top() << ' ';
        s.pop();
    }
}

二、高级部分

2.1 编译器如何实例化模板

模板代码具有两阶段名字查找(two-phase lookup) 特性:

  1. 定义阶段(模板被解析时):查找不依赖于模板参数的名字。
  2. 实例化阶段(模板被实例化时):查找依赖于模板参数的名字(ADL 也参与)。

这意味着:

  • 编译器在遇到使用点时才生成代码。
  • 模板的定义必须对使用点可见——这是模板与普通函数/类最本质的区别。

2.2 程序组织:为什么模板通常放在头文件

由于编译器在实例化时需要看到完整定义,类模板通常 全部写在头文件中:

// stack.hpp
#ifndef STACK_HPP
#define STACK_HPP

template <typename T>
classStack {
public:
voidpush(const T& x){ data.push_back(x); }
// ...
private:
std::vector<T> data;
};

#endif

若坚持分离 .h / .cpp,则需要显式实例化(explicit instantiation):

// stack.cpp
#include"stack.hpp"
templateclassStack<int>;// 显式实例化
templateclassStack<double>;

或使用 extern template 声明抑制重复实例化(C++11):

externtemplateclassStack<int>;// 在其他 TU 中不再实例化

2.3 特化(Specialization)

(1)全特化(Full Specialization)

为某个具体类型提供完全不同的实现:

template <>
classStack<bool> {
public:
voidpush(bool b);   // 可能用位压缩实现
boolpop();
private:
std::vector<unsignedchar> bits;
};

全特化是完全独立的类,与原模板无任何隐式关系。

(2)部分特化(Partial Specialization)

只特化部分模板参数,或对参数施加约束:

template <typename T>
classStack<T*> {// 针对所有指针类型
public:
voidpush(T* p){ data.push_back(p); }
T*   pop(){ T* p = data.back(); data.pop_back(); return p; }
private:
std::vector<T*> data;
};
template <typename T, typename U>
classPair {/* 主模板 */ };

template <typename T>
classPair<T, T> {/* 当两个类型相同时使用 */ };

(3)成员特化

也可以只特化某个成员函数(但必须是全特化,C++ 不允许成员函数部分特化):

template <>
void Stack<constchar*>::push(constchar* const& s) {
// 处理 C 字符串的特殊逻辑
}

2.4 类模板中的名字解析

模板代码中的名字分为两类:

类别
含义
查找时机
非依赖名
不依赖模板参数
定义时查找
依赖名
依赖模板参数
实例化时查找

由于编译器在解析时还不知道 T 是什么,从属类型名必须用 typename 显式标注:

template <typename T>
voidf(){
typename T::value_type* p;   // 必须加 typename
    T::value_type v;             // 也需 typename(此处省略会报错)
}

访问依赖基类成员时,需用 this-> 或 Base<T>:: 限定:

template <typename T>
classDerived :public Base<T> {
public:
voidg(){
this->foo();        // 必须加 this->
        Base<T>::bar();     // 或显式限定
    }
};

对依赖名调用成员模板时,需用 template 关键字:

template <typename T>
voidh(T& c){
    c.template rebind<int>();   // template 消歧
}

2.5 命名空间中的类模板

类模板可以定义在命名空间中,也可对命名空间中的模板做特化(但特化必须与原模板处于同一命名空间):

namespace mylib {

template <typename T>
classVec {/* ... */ };

template <>
classVec<bool> {/* 位压缩特化 */ };

} // namespace mylib

注意:不能向 std 命名空间添加新的类模板,只能对已有模板进行特化(且需符合标准规定)。

2.6 一个较大规模的示例:类型安全的矩阵库

#include<vector>
#include<stdexcept>
#include<iostream>

namespace mat {

template <typename T, std::size_t Rows, std::size_t Cols>
classMatrix {
public:
using value_type = T;
staticconstexprstd::size_t rows = Rows;
staticconstexprstd::size_t cols = Cols;

    Matrix() : data(Rows * Cols, T{}) {}

T&       operator()(std::size_t r, std::size_t c){ return data[r*Cols + c]; }
const T& operator()(std::size_t r, std::size_t c)const{ return data[r*Cols + c]; }

    Matrix& operator+=(const Matrix& rhs) {
for (std::size_t i = 0; i < data.size(); ++i)
            data[i] += rhs.data[i];
return *this;
    }

template <std::size_t K>
    Matrix<T, Rows, K> operator*(const Matrix<T, Cols, K>& rhs) const {
        Matrix<T, Rows, K> result;
for (std::size_t i = 0; i < Rows; ++i)
for (std::size_t j = 0; j < K; ++j) {
                T sum{};
for (std::size_t k = 0; k < Cols; ++k)
                    sum += (*this)(i,k) * rhs(k,j);
                result(i,j) = sum;
            }
return result;
    }

voidprint()const{
for (std::size_t i = 0; i < Rows; ++i) {
for (std::size_t j = 0; j < Cols; ++j)
std::cout << (*this)(i,j) << ' ';
std::cout << '\n';
        }
    }

private:
std::vector<T> data;
};

// 部分特化:1×N 行向量
template <typename T, std::size_t N>
classMatrix<T, 1, N> {
public:
// 不同的实现,例如可省略部分运算
};

} // namespace mat

intmain(){
    mat::Matrix<double, 2, 3> A;
    mat::Matrix<double, 3, 2> B;
for (std::size_t i = 0; i < 2; ++i)
for (std::size_t j = 0; j < 3; ++j) A(i,j) = i + j;
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 2; ++j) B(i,j) = i - j;

auto C = A * B;    // Matrix<double, 2, 2>
    C.print();
}

该示例展示了类模板的诸多高级特性:

  • 非类型模板参数:std::size_t Rows, Cols。
  • 静态常量成员:rows、cols。
  • 成员模板:operator* 返回维度由参数决定的新类型。
  • 部分特化:Matrix<T,1,N>。
  • 命名空间封装。

三、小结

主题
关键点
定义
template <typename T> class X { ... };
成员
成员函数、静态数据成员、嵌套类型、成员模板都可参数化
实例化
使用点触发;定义必须可见(通常置于头文件)
特化
全特化 template<>;部分特化 template<...> class X<...>
名字解析
两阶段查找;依赖名需 typename / template / this->
命名空间
可与普通类一样定义;特化须在同一命名空间

类模板是 C++ 泛型编程的基石,STL 的容器、迭代器、算法几乎全部建立在它之上。掌握类模板的定义、实例化、特化及名字解析规则,是从"使用 STL"迈向"编写可复用库"的关键一步。真正的功力,在于设计出接口清晰、约束得当、编译期可诊断的模板——这正是高级 C++ 程序员的核心竞争力。

相关学习资料