模板把一族相关声明的共同结构写在一起。实例化时,编译器将模板实参与模板形参对应,并检查产生的代码是否合法。本篇采用 C++17,合并原来的“多个模板能否共用一个 T”问答。
模板形参与作用域
跳转到“模板形参与作用域”template<class T> 与 template<typename T> 在这里都声明类型形参,含义相同。class 并不要求传入的类型一定是完整类;是否需要完整定义由具体操作决定,例如指针成员可以引用未完成类型,而直接定义该类型的数据成员通常需要完整类型。模板形参
模板形参只在对应的作用域内有效,不能供后续无关声明复用:
// 错误示范:第二个声明不在第一个模板形参 T 的作用域内。template<class T> struct MyStruct { T value; };class MyClass { T value; }; // T 未声明分别声明模板,或把关联类型嵌套在同一个外层模板中:
#include <cassert>#include <type_traits>
template<class T> struct MyStruct { T value; };template<typename T> struct MyClass { T value; };template<class T> struct Family { struct Item { T value; };};class Incomplete;template<class T> struct Observer { T* pointer = nullptr; };
int main() { MyStruct<int> a{3}; MyClass<double> b{2.5}; Family<int>::Item item{7}; Observer<Incomplete> observer; // 没有构造 Incomplete 对象 static_assert(!std::is_same_v<MyStruct<int>, MyStruct<double>>); assert(a.value == 3 && b.value == 2.5 && item.value == 7); assert(observer.pointer == nullptr);}MyStruct<int> 表示一种具体类型;MyStruct<int> a{3} 才定义对象。不要把“产生一个模板特化类型”与“运行时构造一个对象”混成同一件事。
模板可以在命名空间作用域声明,也可以作为允许的类成员;类可以拥有成员函数模板,并非只能在全局命名空间使用。普通函数体的块作用域不能直接声明模板。模板声明
一个真正保存元素的栈模板
跳转到“一个真正保存元素的栈模板”模板不会消除操作对 T 的要求。这个教学栈用 std::vector<T> 保存元素,push 和返回值需要相应的构造/移动操作;空栈弹出会抛出异常,避免用 T() 假装取出了元素。
#include <cassert>#include <stdexcept>#include <utility>#include <vector>
template<class T>class Stack {public: void push(T value) { values_.push_back(std::move(value)); } bool empty() const { return values_.empty(); } T pop() { if (empty()) throw std::out_of_range("empty stack"); T result = std::move(values_.back()); values_.pop_back(); return result; }private: std::vector<T> values_;};
int main() { Stack<int> stack; stack.push(3); stack.push(8); assert(stack.pop() == 8 && stack.pop() == 3 && stack.empty()); bool rejected = false; try { (void)stack.pop(); } catch (const std::out_of_range&) { rejected = true; } assert(rejected);}这是基本栈行为示例,不承诺任意会抛异常的 T 在移动失败后都保持原值。对于实际项目,可先考虑标准容器适配器 std::stack。
成员模板与依赖基类
跳转到“成员模板与依赖基类”类模板可以有自己的成员模板。继承 Container<T> 时,基类依赖 T,许多基类成员名需要通过 this-> 或基类限定名查找;这不是多态的要求。
#include <cassert>#include <sstream>#include <utility>
template<class T>class Container {public: explicit Container(T value) : data_(std::move(value)) {} const T& getData() const { return data_; } void setData(T value) { data_ = std::move(value); } template<class Stream> void print(Stream& out) const { out << data_; }private: T data_;};
template<class T>class Modifier : public Container<T> {public: using Container<T>::Container; void doubleData() { this->setData(this->getData() * 2); }};
int main() { Modifier<int> number(5); number.doubleData(); std::ostringstream out; number.print(out); assert(number.getData() == 10 && out.str() == "10");}doubleData() 要求 T * 2 的结果可供 setData 使用,并不适用于任意类型;例如 std::string * 2 没有这个含义。模板的“通用”是对满足约束的类型通用。
完全特化、偏特化与重载
跳转到“完全特化、偏特化与重载”| 手段 | 含义 | C++17 中的常见对象 |
|---|---|---|
| 完全特化 | 为确定的模板实参提供实现 | 类、函数、变量模板等 |
| 偏特化 | 为一类满足模式的实参提供实现 | 类模板、变量模板 |
| 函数重载 | 提供同名的不同函数候选 | 普通函数、函数模板;函数模板不能偏特化 |
完全特化是独立的定义,不自动继承主模板的全部成员。例如原笔记提到的 Container<int> 若完全特化,就要自行提供调用方所需的构造、getData、setData 和 print;不能只写一个专用构造函数却假定主模板其余成员还在。显式特化
#include <cassert>#include <sstream>#include <string>
template<class T> struct Box { static constexpr int kind = 0; };template<> struct Box<double> { static constexpr int kind = 1; };template<class T> struct Box<T*> { static constexpr int kind = 2; };
template<class T> constexpr bool isPointer = false;template<class T> constexpr bool isPointer<T*> = true;
template<class T>void print(const T& value, std::ostream& out) { out << value; }template<>void print<std::string>(const std::string& value, std::ostream& out) { out << '[' << value << ']';}
// 用重载表达“指针参数”这一类函数调用,不能写函数模板偏特化。template<class T> int category(const T&) { return 0; }template<class T> int category(T*) { return 1; }
int main() { static_assert(Box<int>::kind == 0 && Box<double>::kind == 1); static_assert(Box<int*>::kind == 2 && isPointer<int*>); static_assert(!isPointer<int>); std::ostringstream out; print(std::string("Qt"), out); assert(out.str() == "[Qt]"); int value = 3; assert(category(value) == 0 && category(&value) == 1);}特化必须在相关的首次隐式实例化之前可见。函数完全特化与重载的选择规则也不同,接口设计中通常优先用清楚的重载表达不同参数形状。
偏特化相交时可能有歧义
跳转到“偏特化相交时可能有歧义”原来的 Box<int, T> 和 Box<T, T> 两种模式都能匹配 Box<int, int>,但彼此并不总是更特殊,因此该交集会产生歧义。可以增加精确特化消解,或重新设计不相交的模式:
#include <cassert>
template<class A, class B> struct PairBox { static constexpr int kind = 0; };template<class T> struct PairBox<int, T> { static constexpr int kind = 1; };template<class T> struct PairBox<T, T> { static constexpr int kind = 2; };template<> struct PairBox<int, int> { static constexpr int kind = 3; };
int main() { static_assert(PairBox<double, char>::kind == 0); static_assert(PairBox<int, double>::kind == 1); static_assert(PairBox<double, double>::kind == 2); static_assert(PairBox<int, int>::kind == 3); assert((PairBox<int, int>::kind == 3));}如果删除最后的完全特化,再使用 PairBox<int, int>,编译器应报歧义,而不是凭书写顺序选择一项。偏特化匹配与排序
模板定义通常放在头文件,让实例化它的翻译单元可见;也可以在清楚限定支持类型时采用显式实例化。这里并入的模板作用域问题来自 原问答。