类模板与 CTAD
使用类模板构建泛型数据结构。理解成员定义、默认参数,以及 C++17 的 CTAD 如何让你省略模板参数。
类模板
类模板定义了由一个或多个类型(或非类型值)参数化的类族。标准库建立在类模板之上:std::vector、std::map、std::optional、std::shared_ptr。当你写 std::vector 时,编译器以 T = int 实例化整个类,生成一个具体的类型。
类模板遵循与函数模板相同的规则:它们必须在头文件中定义,每组唯一的模板参数产生一个独立的类型。Stack 和 Stack 是完全不相关的类型 ── 它们之间没有继承关系。
一个泛型栈
这是一个简单的泛型栈。注意在类体外定义的成员函数需要重复模板参数列表。
#include <vector>
#include <stdexcept>
#include <iostream>
template <typename T>
class Stack {
private:
std::vector<T> elements_;
public:
void push(const T& value);
void pop();
const T& top() const;
bool empty() const { return elements_.empty(); }
std::size_t size() const { return elements_.size(); }
};
// Member definitions outside the class require the template header
template <typename T>
void Stack<T>::push(const T& value) {
elements_.push_back(value);
}
template <typename T>
void Stack<T>::pop() {
if (elements_.empty()) {
throw std::out_of_range("Stack<>::pop(): empty stack");
}
elements_.pop_back();
}
template <typename T>
const T& Stack<T>::top() const {
if (elements_.empty()) {
throw std::out_of_range("Stack<>::top(): empty stack");
}
return elements_.back();
}
int main() {
Stack<int> intStack;
intStack.push(42);
intStack.push(17);
std::cout << intStack.top() << '\n'; // 17
intStack.pop();
std::cout << intStack.top() << '\n'; // 42
}多个参数与默认值
类模板可以有多个模板参数和默认模板参数,就像函数参数可以有默认值一样。默认参数从左到右解析。
#include <vector>
#include <deque>
#include <iostream>
// Container defaults to std::deque<T> — matches std::stack's design
template <typename T, typename Container = std::deque<T>>
class Stack {
private:
Container elements_;
public:
void push(const T& value) { elements_.push_back(value); }
void pop() { elements_.pop_back(); }
const T& top() const { return elements_.back(); }
bool empty() const { return elements_.empty(); }
};
int main() {
Stack<int> s1; // uses deque<int>
Stack<int, std::vector<int>> s2; // uses vector<int>
s1.push(10);
s2.push(20);
std::cout << s1.top() << '\n'; // 10
std::cout << s2.top() << '\n'; // 20
}类模板参数推导(CTAD)
在 C++17 之前,你在构造类模板实例时总是需要指定模板参数:std::pair。C++17 引入的 CTAD(类模板参数推导)让编译器能从构造函数参数推导类模板参数,就像函数模板参数推导一样。
使用 CTAD:std::pair p(1, 3.14); ── 编译器推导出 std::pair。这适用于 std::vector、std::tuple、std::optional、std::array(C++20),以及你自己的类模板。
CTAD 使用推导指引 ── 隐式的(从构造函数生成)或显式的(用户定义)。编译器尝试每个指引并选择最佳匹配。
CTAD 与推导指引
用户定义的推导指引告诉编译器如何将构造函数参数映射到模板参数。当构造函数参数类型与模板参数不直接对应时,它们尤其有用。
#include <iostream>
#include <cstddef>
template <typename T, std::size_t N>
struct Array {
T data[N];
};
// Without a deduction guide, CTAD cannot deduce T and N.
// This explicit deduction guide maps brace-init with N elements
// of the same type T to Array<T, N>.
template <typename First, typename... Rest>
Array(First, Rest...) -> Array<First, 1 + sizeof...(Rest)>;
// Standard library example: std::vector CTAD from iterators
#include <vector>
#include <list>
int main() {
// CTAD with our custom deduction guide
Array arr{1, 2, 3, 4, 5}; // deduces Array<int, 5>
std::cout << arr.data[2] << '\n'; // 3
// Standard library CTAD examples (C++17)
std::pair p(42, 3.14); // pair<int, double>
std::vector v{1, 2, 3}; // vector<int>
std::tuple t(1, 2.0, 'a'); // tuple<int, double, char>
// CTAD from iterator range
std::list<int> lst{10, 20, 30};
std::vector v2(lst.begin(), lst.end()); // vector<int>
}成员模板
类模板的成员函数本身可以是模板,添加额外的模板参数。这称为成员模板。一个常见用例是允许从相关但不同的实例化进行赋值或构造。
例如,你可能想将 Stack 从 Stack 赋值 ── 但由于这些是不相关的类型,默认的拷贝赋值不起作用。成员模板赋值运算符可以解决这个问题:
template <typename T>
class Stack {
public:
template <typename U>
Stack& operator=(const Stack<U>& other);
};在这个成员模板中,U 可能与 T 不同。只要 U 可以转换为 T,函数体就可以从 other 复制元素。这种模式在标准库中被广泛使用 ── std::shared_ptr 通过成员模板构造函数转换为 std::shared_ptr。
当你特化一个类模板时,特化是一个全新的类 ── 它不从主模板继承任何成员。你必须重新定义每个成员函数和数据成员。如果忘记在特化中重新定义某个成员,在使用该成员时会导致编译错误。此外,偏特化仅适用于类模板,不适用于函数模板。我们将在下一章详细介绍特化。
- 类模板定义类族 ── 每组唯一的参数产生一个独立的类型
- 在类外定义的成员函数必须重复
template头部,并使用ClassName:: - 默认模板参数从左到右生效,就像默认函数参数一样
- CTAD(C++17)让编译器从构造函数参数推导类模板参数
- 显式推导指引将构造函数参数模式映射到模板参数
- 成员模板允许在同一类模板的不同实例化之间进行操作
测验 — 检验你的知识
(15 经验值)1. 给定 `template <typename T, typename Container = std::deque<T>> class Stack`,`Stack<int>` 的类型是什么?
2. 什么是 CTAD(C++17)?
3. 当你特化一个类模板时,主模板的成员会怎样?