跳转到内容
新建笔记

C++ 模板:作用域、成员模板与特化

模板把一族相关声明的共同结构写在一起。实例化时,编译器将模板实参与模板形参对应,并检查产生的代码是否合法。本篇采用 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>,编译器应报歧义,而不是凭书写顺序选择一项。偏特化匹配与排序

模板定义通常放在头文件,让实例化它的翻译单元可见;也可以在清楚限定支持类型时采用显式实例化。这里并入的模板作用域问题来自 原问答。