特化(Specialization)
目录
25.2 模板参数和参数(Template Parameters and Arguments)
25.2.1 类型作为参数(Types as arguments )
25.2.1 值作为参数(Values as arguments )
25.2.3 运算作为参数(Operations as arguments )
25.2.4 模板作为参数(Templates as arguments )
25.2.5 默认模板参数(Default Templates Arguments )
25.2.5.1 默认函数模板参数(Default Function Template Arguments )
25.3 特化(专门化,指定参数类型的模板)(Specialization)
25.3.1 接口特化(Interface Specialization)
25.3.1.1 实现特化(Implementation Specialization)
25.3.2 主模板(The Primary Template)
25.3.3 特化顺序(Order of Specialization)
25.3.4 函数模板特化(Function Template Specialization)
25.3.1.1 特化和重载(Specialization and Overloading)
25.3.1.2 非重载的特化(Specialization That Is Not Overloading)
25.1 引言(Introduction)
在过去的二十年里,模板从一个相对简单的思想发展成为大多数高级 C++ 编程的基石。特别是,模板是以下技术的关键:
• 提高类型安全性(例如,通过消除强制类型转换的使用以达到类型安全;§12.5);
• 提高程序的一般抽象级别(例如,通过使用标准容器和算法;§4.4,§4.5,§7.4.3,第 31 章,第 32 章);以及
• 提供更灵活、类型安全和高效的类型和算法参数化(§25.2.3)。
这些技术都严重依赖于模板代码以无开销和类型安全的方式使用模板参数的能力。大多数技术还依赖于模板提供的类型推断机制(有时称为编译时多态性;§27.2)。这些技术是 C++ 在性能关键领域(例如高性能数值计算和嵌入式系统编程)使用的基石。有关成熟示例,请参阅标准库(第 IV 部分)。
本章和接下来的两章介绍了支持旨在实现无妥协灵活性和性能的技术的高级和/或专用语言功能的简单示例。其中许多技术主要为库实现者开发和使用。像大多数程序员一样,我大多数时候都喜欢忘记更高级的技术。只要可以,我就会保持代码简单并依赖库,这样我就可以从特定应用领域的专家手中使用高级功能中受益。
模板在 §3.4 中介绍。本章是介绍模板及其用途的系列文章的一部分:
• 第 23 章 对模板进行了更详细的介绍。
• 第 24 章 讨论了模板最常见的用途,即泛型编程。
• 第 25 章(本章) 展示了如何使用一组参数专门化模板。
• 第 26 章 重点介绍与名称绑定相关的模板实现问题。
• 第 27 章 讨论了模板和类层级结构之间的关系。
• 第 28 章 重点介绍模板作为生成类和函数的语言。
• 第 29 章 介绍了基于模板的编程技术的更大示例。
25.2 模板参数和参数(Template Parameters and Arguments)
模板可以取的参数(parameters)有:
• “类型的类型” 的类型参数
• 内置类型的值参数,例如 int(§25.2.2)和指向函数的指针(译注:指针也是整数值)(§25.2.3)
• “类型模板”的模板参数(§25.2.4)
类型的类型是迄今为止最常见的,但值类型对于许多重要的技术来说是必不可少的(§25.2.2,§28.3)。
模板可以采用固定数量的参数,也可以采用可变数量的参数。可变参数模板的讨论将推迟到 §28.6。
请注意,通常使用首字母大写的短名称作为模板参数类型的名称,例如 T、C、Cont 和 Ptr。这是可以接受的,因为这些名称往往是常规的并且限制在相对较小的范围内(§6.3.3)。但是,当使用 ALL_CAPS 时,总是有可能与宏发生冲突(§12.6),因此不要使用足够长的名称来与可能的宏名称发生冲突。
25.2.1 类型作为参数(Types as arguments )
模板参数(template argument)通过在类型参数前加上typename或class来进行定义。使用其中任何一种的结果都是完全等效的。每一种类型(内置或用户定义)在语法上都可接受声明为采用类型参数的模板。例如:
template<typename T>
void f(T);
template<typename T>
class X {
// ...
};
f(1); //T 推导为 int
f<double>(1); //T 是double
f<complex<double>>(1); // T 是 complex<double>
X<double> x1; // T is double
X<complex<double>> x2; // T 是 complex<double>
类型参数不受约束;也就是说,类的接口中没有任何内容将其限制为某种类型或类层次结构的一部分。参数类型的有效性完全取决于它在模板中的使用,从而提供了一种鸭子类型 (§24.1)。你可以将一般约束实现为概念(concepts) (§24.3)。
当用作模板参数时,用户定义类型和内置类型可以同等处理。这对于允许我们定义对用户定义类型和内置类型工作方式相同的模板至关重要。例如:
vector<double> x1; // double型vector
vector<complex<double>> x2; // complex<double>型vector
特别是,与另一个相比,使用其中任何一个都不会产生空间或时间开销:
• 内置类型的值不会被“装箱”到特殊容器对象中。
• 所有类型的值都直接从vector中检索,而无需使用可能昂贵的(例如虚的)“get() 函数”。
• 用户定义类型的值不会通过引用隐式访问。
要用作模板参数,类型必须在作用域内且可访问。例如:
class X {
class M { /* ... */ };
// ...
void mf();
};
void f()
{
struct S { /* ... */ };
vector<S> vs; // OK
vector<X::M> vm; // 错: X::M 是私有的
// ...
}
void M::mf()
{
vector<S> vs; // 错: 作用域内无 S
vector<M> vm; // OK
// ...
};
25.2.1 值作为参数(Values as arguments )
非类型或模板的模板参数称为值参数,传递给它的参数称为值参数。例如,整数参数可用于提供大小和限制:
template<typename T, int max>
class Buffer {
T v[max];
public:
Buffer() { }
// ...
};
Buffer<char,128> cbuf;
Buffer<int,5000> ibuf;
Buffer<Record,8> rbuf;
在运行时效率和紧凑性至关重要的情况下,简单且受约束的容器(例如 Buffer)可能很重要。它们避免使用更通用的字符串或向量所暗示的自由存储,同时不会像内置数组那样受到隐式转换为指针的影响(§7.4)。标准库数组(§34.2.1)实现了这个思想。
模板值参数的参数可以是(§iso.14.3.2):
• 整型常量表达式 (§10.4)
• 指向具有外部链接的对象或函数的指针或引用 (§15.2)
• 指向成员的非重载指针 (§20.6)
• 空指针 (§7.2.2)
用作模板参数的指针必须采用 &of 形式,其中 of 是对象或函数的名称,或采用 f 形式,其中 f 是函数的名称。指向成员的指针必须采用 &X::of 形式,其中 of 是成员的名称。特别是,字符串文字量不能直接用作模板参数:
template<typename T, char∗ label>
class X {
// ...
};
X<int,"BMW323Ci"> x1; // 错: string 文字量作为模板参数
char lx2[] = "BMW323Ci";
X<int,lx2> x2; // OK: lx2 有外部链接
与针对浮点模板值参数的限制一样,此限制的存在是为了简化单独编译的编译单元的实现。最好将模板值参数视为将整数和指针传递给函数的机制。抵制尝试更聪明的方法的诱惑。遗憾的是(没有根本原因),文字类类型(§10.4.3)不能用作模板值参数。值模板参数是一些更高级的编译时计算技术的机制(第 28 章)。
整数模板参数必须是常量(译注:即不能是变量)。例如:
constexpr int max = 200;
void f(int i)
{
Buffer<int,i> bx; // 错: 期望常量表达式
Buffer<int,max> bm; // OK: 常量表达式
// ...
}
反之,值模板参数是模板内的常量,因此尝试更改参数的值是错误的。例如:
template<typename T, int max>
class Buffer {
T v[max];
public:
Buffer(int i) { max = i; } // 错: 试图赋值给模板参数
// ...
};
类型模板参数稍后可用作模板参数列表中的类型。例如:
template<typename T, T default_value>
class Vec {
// ...
};
Vec<int,42> c1;
Vec<string,""> c2;
当与默认模板参数 (§25.2.5) 结合使用时,这变得特别有用;例如:
template<typename T, T default_value = T{}>
class Vec {
// ...
};
Vec<int,42> c1;
Vec<int> c11; // default_value 是 int{}, 即, 0
Vec<string,"for tytwo"> c2;
Vec<string> c22; // default_value 是 string{}; 即, ""
25.2.3 运算作为参数(Operations as arguments )
考虑标准库map(§31.4.3)的稍微简化的版本:
template<typename Key, Class V>
class map {
// ...
};
我们如何提供 Key 的比较标准?
• 我们不能将比较标准硬编码到容器中,因为容器(一般来说)不能将其需求强加于元素类型。例如,默认情况下,map使用 < 进行比较,但并非所有 Key 都有我们想要使用的 <。
• 我们不能将排序标准硬编码到 Key 类型中,因为(一般来说)基于 key 对元素进行排序的方式有很多种。例如,最常见的 Key 类型之一是string,并且可以根据各种标准(例如区分大小写和不区分大小写)对string进行排序。
因此,排序标准并未内置于容器类型或元素类型中。原则上,map的排序标准概念可以表示为:
[1] 模板值参数(例如,指向比较函数的指针)
[2] map模板的模板类型参数,确定比较对象的类型
乍一看,第一个解决方案(传递特定类型的比较对象)似乎更简单。例如:
template<typename Key, typename V, bool(∗cmp)(const Key&, const Key&)>
class map {
public:
map();
// ...
};
该map要求用户提供比较函数:
bool insensitive(const string& x, const string& y)
{
// compare case insensitive (e.g., "hello" equals "HellO")
}
map<string,int,insensitive> m; // 忽略大小写的比较
但是,这种方式不太灵活。特别是,map 的设计者必须决定是否使用指向函数的指针或某个特定类型的函数对象来比较(未知的)Key 类型。此外,由于比较运算符的参数类型必须依赖于 Key 类型,因此很难提供默认的比较标准。
因此,第二种选择(将比较的类型作为模板类型参数传递)是更常见的,也是标准库中使用的选择。例如:
template<typename Key, Class V, typename Compare = std::less<Key>>
class map {
public:
map() { /* ... */ } // 使用默认比较
map(Compare c) :cmp{c} { /* ... */ } //重写默认
// ...
Compare cmp {}; // 默认比较
};
最常见的情况是使用小于进行比较,这是默认情况。如果我们想要一个不同的比较标准,我们可以将其作为函数对象提供(§3.4.3):
map<string,int> m1; // 使用默认比较 (less<string>)
map<string,int,std::greater<string>> m2; // 使用 greater<string>()比较
函数对象可以携带状态。例如:
Complex_compare f3 {"French",3}; // 创建一个比较对象 (§25.2.5)
map<string,int,Complex_compare> m3 {f3}; // 使用f3()比较
我们还可以使用指向函数的指针,包括可以转换为指向函数的指针的 lambda(§11.4.5)。例如:
using Cmp = bool(∗)(const string&,const string&);
map<string,int,Cmp> m4 {insensitive}; // compare using a pointer to function
map<string,int,Cmp> m4 {[](const string& a, const string b) { return a>b; } };
与传递函数指针相比,将比较操作作为函数对象传递具有显著的优势:
• 在类中定义的简单类成员函数对于内联来说很简单,而通过指向函数的指针内联调用则需要编译器特别注意。
• 可以传递没有数据成员的函数对象,而无需任何运行时成本。
• 可以将多个操作作为单个对象传递,而无需额外的运行时成本。
map的比较标准只是一个例子。但是,传递它所使用的技术是通用的,并且非常广泛地用于使用“策略”参数化类和函数。示例包括算法的操作(§4.5.4,§32.4),容器的分配器(§31.4,§34.4)和unique_ptr的删除器(§34.3.1)。当我们需要为函数模板(例如sort())指定参数时,我们有相同的设计替代方案,标准库也为这些情况选择了替代方案[2](例如,参见§32.4)。
如果我们的程序中只使用一次比较标准,那么使用 lambda 来更简洁地表达函数对象版本可能更有意义:
map<string,int,Cmp> c3 {[](const string& x, const string& y) const { return x<y; }}; // error
不幸的是,这行不通,因为没有将 lambda 转换为函数对象类型。我们可以命名 lambda,然后使用该名称:
auto cmp = [](const string& x, const string& y) const { return x<y; }
map<string,int,decltype(cmp)> c4 {cmp};
我发现从设计和维护的角度来看命名操作很有用。此外,任何非本地命名和声明的东西都可能有其他用途。
25.2.4 模板作为参数(Templates as arguments )
有时将模板(而不是类或值)作为模板参数传递很有用。例如:
template<typename T, template<typename> class C>
class Xrefd {
C<T> mems;
C<T∗> refs;
// ...
};
template<typename T>
using My_vec = vector<T>; // 使用默认分配符
Xrefd<Entry,My_vec> x1; // 在一个vector中存储交Entry的叉参考
template<typename T>
class My_container {
// ...
};
Xrefd<Record,My_container> x2; //在一个My_container中存储Record的交叉参考
要将模板声明为模板参数,我们必须指定其必需的参数。例如,我们指定 Xrefd 的模板参数 C 是一个采用单一类型参数的模板类。如果我们不这样做,我们将无法使用 C 的特化。使用模板作为模板参数的目的通常是我们希望使用各种参数类型(例如上例中的 T 和 T∗)来实例化它。也就是说,我们想用另一个模板来表达模板的成员声明,但我们希望另一个模板是一个参数,以便用户可以指定它。
只有类模板可能是模板参数。
模板只需要一个或两个容器的常见情况通常通过传递容器类型(§31.5.1)来更好地处理。例如:
template<typename C, typename C2>
class Xrefd2 {
C mems;
C2 refs;
// ...
};
Xrefd2<vector<Entry>,set<Entry∗>> x;
这里,C 和 C2 的值类型可以通过用于获取容器元素类型的简单类型函数(§28.2)获得,例如 Value_type<C>。这是用于标准库容器适配器(例如队列(§31.5.2))的技术。
25.2.5 默认模板参数(Default Templates Arguments )
每次使用 map 时都明确指定比较标准是一件很繁琐的事情——尤其是当 less<Key> 通常是最佳选择时。我们可以将 less<Key> 指定为 Compare 模板参数的默认类型,这样只需要明确指定不常见的比较标准:
template<typename Key, Class V, typename Compare = std::less<Key>>
class map {
public:
explicit map(const Compare& comp ={});
// ...
};
map<string,int> m1; // 将使用less<string>进行比较
map<string,int,less<string>> m2; // 与 m1 类型相同
struct No_case {
// 定义operator()() 以进行忽略大小写的字符串比较
};
map<string,int,No_case> m3; // m3 与 m1 和 m2 的类型都不同
请注意,默认的 map 构造函数如何创建默认的比较对象 Compare{}。这是常见情况。如果我们想要更复杂的构造,我们必须明确地这样做。例如:
map<string,int,Complex_compare> m {Complex_compare{"French",3}};
仅当实际使用了默认参数时,才会对模板参数的默认参数进行语义检查。具体来说,只要我们不使用默认模板参数 less<Key>,我们就可以 compare() 类型 X 的值,而 less<X> 无法编译。这一点在标准容器(例如 std::map)的设计中至关重要,它依赖于模板参数来指定默认值(§31.4)。
正如默认函数参数(§12.2.5)一样,可以指定默认模板参数,并且仅将其提供给尾随参数:
void f1(int x = 0, int y); // 错 : 默认参数不尾随
void f2(int x = 0, int y = 1); // OK
f2(,2); // 语法错误
f2(2); // call f(2,1);
template<typename T1 = int, typename T2>
class X1 { // 错 : 默认参数不尾随
// ...
};
template<typename T1 = int, typename T2 = double>
class X2 { // OK
// ...
};
X2<,float> v1; // 语法错误
X2<float> v2; // v2 是一个X2<float,double>
不允许“空”参数意味着“使用默认值”是在灵活性和出现隐蔽错误的可能性之间的一种刻意权衡。
通过模板参数提供策略,然后将该参数设为默认以提供最常见策略的这种技术在标准库中几乎是通用的(例如,§32.4)。奇怪的是,它不用于 basic_string(§23.2,第 36 章)比较。相反,标准库字符串依赖于char_traits(§36.2.2)。类似地,标准算法依赖于 iterator_traits(§33.1.3),标准库容器依赖于allocators(§34.4)。特征(traits)的使用在 §28.2.4 中介绍。
25.2.5.1 默认函数模板参数(Default Function Template Arguments )
当然,默认模板参数对于函数模板也很有用。例如:
template<typename Target =string, typename Source =string>
Target to(Source arg) // 将Source 转换到 Target
{
stringstream interpreter;
Targ et result;
if (!(interpreter << arg) // 将arg写入流
|| !(interpreter >> result) // 从流读取
|| !(interpreter >> std::ws).eof()) // 流中剩余的东西?
throw runtime_error{"to<>() failed"};
return result;
}
仅当无法推断或没有默认值时,才需要明确提及函数模板参数,因此我们可以写出:
auto x1 = to<string,double>(1.2); // 非常显式 (且冗长)
auto x2 = to<string>(1.2); // Source 定义为 double
auto x3 = to<>(1.2); // Target 默认为string; Source 推导为 double
auto x4 = to(1.2); // <> 是多余的
如果所有函数模板参数都是默认的,则可以省略<> (与函数模板特化完全一样;§25.3.4.1)。
to() 的这个实现对于简单类型的组合来说有点重量级,例如 to<double>(int),但改进的实现可以作为特化提供(§25.3)。请注意,to<char>(int) 不起作用,因为 char 和 int 不共享字符串表示。对于标量数值类型之间的转换,我倾向于使用 narrow_cast<>()(§11.5)。
25.3 特化(专门化,指定参数类型的模板)(Specialization)
默认情况下,模板会为用户能想到的每个模板参数(或模板参数组合)提供一个定义。这对于编写模板的人来说并不总是有意义的。我可能想说,“如果模板参数是指针,则使用这个实现;否则,则使用那个实现”,或“除非模板参数是从 My_base 类派生的指针,否则给出错误。”许多此类设计问题可以通过提供模板的替代定义并让编译器基于提供的模板参数在它们使用的地方进行选择来解决。模板的此类替代定义称为用户定义的特化,或简称为用户特化。考虑 Vector 的可能用途:
template<typename T>
class Vector { // 通用vector 类型
T∗ v;
int sz;
public:
Vector();
explicit Vector(int);
T& elem(int i) { return v[i]; }
T& operator[](int i);
void swap(Vector&);
// ...
};
Vector<int> vi;
Vector<Shape∗> vps;
Vector<string> vs;
Vector<char∗> vpc;
Vector<Node∗> vpn;
在这样的代码中,大多数 Vector 都是某种指针类型的 Vector。这有几个原因,但主要原因是,为了保留运行时多态行为,我们必须使用指针(§3.2.2,§20.3.2)。也就是说,任何实践面向对象编程并使用类型安全容器(例如标准库容器)的人最终都会得到大量指针容器。
大多数 C++ 实现的默认行为是复制模板函数的代码。这通常有利于提高运行时性能,但除非小心谨慎,否则在关键情况下(例如 Vector 示例)会导致代码膨胀。
幸运的是,有一个显而易见的解决方案。指针容器可以共享一个实现。这可以通过特化来表达。首先,我们为指向 void 的指针定义一个 Vector 版本(特化):
template<>
class Vector<void∗>{ // 完化特化
void∗∗ p;
// ...
void∗& operator[](int i);
};
然后,此特化可用作所有指针向量的通用实现。另一种用途是基于存储 void∗ 的单个共享实现类来实现 unique_ptr<T>。
template<> 前缀表示这是一个无需模板参数即可指定的特化。特化所针对的模板参数在名称后的 <> 括号中指定。也就是说,<void∗> 表示此定义将用作 T 为 void∗ 的每个 Vector 的实现。
Vector<void∗> 是完全特化(译注:即直接指定特定类型)。也就是说,当我们使用特化时,无需指定或推断模板参数;Vector<void∗> 用于如下声明的 Vector:
Vector<void∗> vpv;
要定义一个用于每个指针向量且仅用于指针向量的特化,我们可以这样写:
template<typename T>
class Vector<T∗> : private Vector<void∗> { // 部分特化
public:
using Base = Vector<void∗>;
Vector() {}
explicit Vector(int i) : Base(i) {}
T∗& elem(int i) { return reinterpret_cast<T∗&>(Base::elem(i)); }
T∗& operator[](int i) { return reinterpret_cast<T∗&>(Base::operator[](i)); }
// ...
};
名称后面的特化模式 <T∗> 表示此特化将用于每个指针类型;也就是说,此定义将用于每个具有可以表示为 T∗ 的模板参数的 Vector。例如:
Vector<Shape∗> vps; // <T*> 是 <Shape*> ,因此 T 是 Shape
Vector<int∗∗> vppi; // <T*> 是 <int**>,因此 T is int*
包含模板参数的模式(pattern)之特化称为部分特化(译注:即指定一个通用类型),与完全特化(如 vector<void∗> 的定义)相对,其中“模式”只是一种特定类型。
请注意,当使用部分特化时,模板参数是从特化模式推导出来的;模板参数不仅仅是实际的模板参数。特别是,对于 Vector<Shape∗>,T 是 Shape 而不是 Shape∗ 。
鉴于 Vector 的这种部分特化,我们为所有指针向量提供了一个共享实现。Vector<T∗> 类只是 Vector<void∗> 的一个接口,通过派生和内联扩展专门实现。
重要的是,在不影响呈现给用户的接口的情况下实现 Vector 实现的这种改进(译注:这里的用户指接口的调用者,在调用者看来,这是个通用接口,并无异样,但实则其内部实现可能针对不同的用途作了处理,即特化了)。特化是一种为通用接口的不同用途指定替代实现的方式。当然,我们可以给通用 Vector 和指针 Vector 赋予不同的名称。然而,当我尝试这样做时,许多应该知道的人忘记使用指针类,发现他们的代码比预期的要大得多。在这种情况下,最好将关键的实现细节隐藏在通用接口后面。
事实证明,特化技术在实际使用中可以成功抑制代码膨胀。不使用这种技术的人(在 C++ 或其他具有类似类型参数化功能的语言中)会发现,即使在中等大小的程序中,复制代码也会占用数兆字节的代码空间。通过消除编译这些附加版本的 Vector 操作所需的时间,这种技术还可以大幅缩短编译和链接时间。使用单一特化来实现所有指针列表是通过最大化共享代码量来最小化代码膨胀的通用技术的一个示例。
一些编译器已经足够智能,无需程序员的帮助就能执行这种特殊的优化,但这种技术普遍有用且适用。
使用单一运行时表示来表示多种类型的值,并依赖(静态)类型系统来确保仅根据其声明的类型使用的这种技术变体称为类型擦除(type erasure)。在 C++ 的上背景下,它首次记录在原始模板论文 [Stroustrup,1988] 中。
25.3.1 接口特化(Interface Specialization)
有时,特化不是算法优化,而是对接口(甚至表示)的修改。例如,标准库 complex 使用特化来调整构造函数集和重要操作的参数类型,以实现重要的特化(例如 complex<float> 和 complex<double>)。通用(主要)模板(§25.3.1.1)如下所示:
template<typename T>
class complex {
public:
complex(const T& re = T{}, const T& im = T{});
complex(const complex&); // 复制构造函数
template<typename X>
complex(const complex<X>&); // 从complex<X> 转换至 complex<T>
complex& operator=(const complex&);
complex<T>& operator=(const T&);
complex<T>& operator+=(const T&);
// ...
template<typename X>
complex<T>& operator=(const complex<X>&);
template<typename X>
complex<T>& operator+=(const complex<X>&);
// ...
};
请注意,标量赋值运算符采用引用参数。这对于浮点数来说效率不高,因此 complex<float> 按值传递这些浮点数:
template<>
class complex<float> {
public:
// ...
complex<float>& operator= (float);
complex<float>& operator+=(float);
// ...
complex<float>& operator=(const complex<float>&);
// ...
};
对于 complex<double>,同样的优化也适用。此外,还提供了从 complex<float> 和 complex<long double> 的转换(如 §23.4.6 中所述):
template<>
class complex<double> {
public:
constexpr complex(double re = 0.0, double im = 0.0);
constexpr complex(const complex<float>&);
explicit constexpr complex(const complex<long double>&);
// ...
};
请注意,这些专门的构造函数是 constexpr,使 complex<double> 成为文字量类型。对于一般的 complex<T>,我们无法做到这一点。此外,此定义利用了以下知识:从 complex<float> 到 complex<double> 的转换是安全的(它永远不会窄化),因此我们可以从 complex<float> 获得隐式构造函数。但是,来自 complex<long double> 的构造函数是显式的,以减少窄化的可能性。
25.3.1.1 实现特化(Implementation Specialization)
特化可用于为特定模板参数集提供类模板的替代实现。在这种情况下,特化甚至可以提供与通用模板不同的表示。例如:
template<typename T, int N>
class Matrix; // T的N维矩阵
template<typename T,0>
class Matrix { // 对N==0 的特化
T val;
// ...
};
template<typename T,1>
class Matrix { // 对 N=1的特化
T∗ elem;
int sz; // 元素数
// ...
};
template<typename T,2>
class Matrix { // 对 N=2 的特化
T∗ elem;
int dim1; // 行数
int dim2; // 列数
// ...
};
25.3.2 主模板(The Primary Template)
当我们既有模板的通用定义,又有定义特定模板参数集实现的特化时,我们将最通用的模板称为主模板。主模板定义所有特化的接口(§iso.14.5.5)。也就是说,主模板用于确定使用是否有效并参与重载解析。只有在选择了主模板后才会考虑特化。
主模板必须在任何特化之前声明。例如:
template<typename T>
class List<T∗> {
// ...
};
template<typename T>
class List { //错误: 特化之后的主模板
// ...
};
主模板提供的关键信息是一组模板参数,用户必须提供这些参数才能使用它或其任何特化。如果我们为模板定义了约束检查(§24.4),则主模板就是它所属的位置,因为概念是用户关心的东西,必须理解才能使用模板。例如:
template<typename T>
class List {
static_assert(Regular<T>(),"List<T>: T must be Regular");
// ...
};
主模板的声明足以允许定义特化(译注:即主模板声明之后即可定义特化,不一定非得定义之后):
template<typename T>
class List; // 不是一个定义
template<typename T>
class List<T∗> {
// ...
};
如果使用,则必须在某处定义主模板(§23.7)。如果主模板从未实例化,则无需定义。这可用于定义仅接受一组固定替代参数的模板。如果用特化模板,则该特化必须在每次使用该模板时都处于其特化类型的作用域内。例如:
template<typename T>
class List {
// ...
};
List<int∗> li;
template<typename T>
class List<T∗>{ // 错: 使用未定义的特化
// ...
};
这里,在使用 List<int∗> 之后,List 被特化为 int∗。(译注:对于这种情况,有的编译器可能认为只要有特化定义即可,不管定义顺序如何,编译器会对编译顺序作调整。)
对于给定的一组模板参数,模板的每次使用都必须通过相同的特化来实现。否则,类型系统就会被破坏,因此,在不同的地方使用相同的模板可能会产生不同的结果,并且在程序的不同部分创建的对象可能不兼容。显然,这将是灾难性的,因此程序员必须注意显式特化在整个程序中是一致的。在原则上,实现能够检测不一致的特化,但标准并不要求它们这样做,有些则不需要。
模板的所有特化都必须在与主模板相同的命名空间中声明。如果使用,则显式声明的特化(而不是从更通用的模板生成的特化)也必须在某处显式定义(§23.7)。换句话说,显式特化模板意味着不会为该特化生成(其他)定义。
25.3.3 特化顺序(Order of Specialization)
如果与某个特化模式匹配的每个参数列表也与另一个匹配,则一个特化比另一个特化更特化,但反之则不然。例如:
template<typename T>
class Vector; // 通用; 主模板
template<typename T>
class Vector<T∗>; //对任意类型指针的特化
template<>
class Vector<void∗>; // 对void* 型的特化
每种类型都可以用作最通用的 Vector 的模板参数,但 Vector<T∗> 只能使用指针,而 Vector<void∗> 只能使用 void∗。
在对象、指针等的声明中,最特化的版本将优先于其他版本(§25.3)。
25.3.4 函数模板特化(Function Template Specialization)
特化对于模板函数也很有用(§25.2.5.1)。但是,我们可以重载函数,因此特化较少。此外,C++ 仅支持函数的完全特化(§iso.14.7),因此我们在可能尝试过部分特化的地方使用重载。
25.3.1.1 特化和重载(Specialization and Overloading)
考虑 §12.5 和 §23.5 中的 Shell 排序。这些版本使用 < 比较元素,并使用详细代码交换元素。更好的定义是:
template<typename T>
bool less(T a, T b)
{
return a<b;
}
template<typename T>
void sort(Vector<T>& v)
{
const size_t n = v.siz e();
for (int gap=n/2; 0<gap; gap/=2)
for (int i=gap; i!=n; ++i)
for (int j=i−gap; 0<=j; j−=gap)
if (less(v[j+gap],v[j]))
swap(v[j],v[j+gap]);
}
这不会改进算法本身,但可以改进算法的实现。现在,我们有 less 和 swap 作为命名实体,我们可以为其提供改进的版本。这些名称通常称为自定义点。
正如所写,sort() 不会正确地对 Vector<char∗> 进行排序,因为 < 将比较两个 char∗。也就是说,它将比较每个字符串中第一个字符的地址。相反,我们希望它比较指向的字符。对 const char∗ 进行 less() 的简单特化将解决此问题:
template<>
bool less<const char∗>(const char∗ a, const char∗ b)
{
return strcmp(a,b)<0;
}
至于类(§25.3),template<> 前缀表示这是一个无需模板参数即可指定的特化。模板函数名称后面的 <const char∗> 表示此特化将在模板参数为 const char∗ 的情况下使用。由于模板参数可以从函数参数列表中推导出来,因此我们无需明确指定它。因此,我们可以简化特化的定义:
template<>
bool less<>(const char∗ a, const char∗ b)
{
return strcmp(a,b)<0;
}
鉴于 template<> 前缀,第二个空 <> 是多余的,因此我们通常简单地写:
bool less(const char∗ a, const char∗ b)
{
return strcmp(a,b)<0;
}
现在我们已经将less()“特化”为语义正确的版本,我们可以考虑对 swap() 做些什么。标准库 swap() 适合我们的使用,并且已经针对具有高效移动操作的每种类型进行了优化。因此,当我们使用 swap() 代替三个可能昂贵的复制操作时,我们提高了大量参数类型的性能。
当参数类型的不规则性导致通用算法产生不理想的结果(例如 C 风格字符串的 less())时,特化就派上用场了。这些“不规则类型”通常是内置指针和数组类型。
25.3.1.2 非重载的特化(Specialization That Is Not Overloading)
特化与重载有何不同?从技术角度来看,它们的区别在于,单个函数参与重载,而只有主模板参与特化(§25.3.1.1)。但是,我想不出一个实际区别的例子。
函数特化有几种用途。例如,我们可以在不带参数的函数中进行选择:
template<typename T> T max_value(); // 未定义
template<> constexpr int max_value<int>() { return INT_MAX; }
template<> constexpr char max_value<char>() { return CHAR_MAX; }
//...
template<typename Iter>
Iter my_algo(Iter p)
{
auto x = max_value<Value_type<Iter>>(); // 适用于具有特化的 max_value() 的类型// ...
}
我使用类型函数 Value_type<> 来获取 Iter 指向的对象的类型(§24.4.2)。
为了获得与重载大致相同的效果,我们必须传递一个虚(dummy)(未使用的)参数。例如:
int max2(int) { return INT_MAX; }
char max2(char) { return INT_MAX; }
template<typename Iter>
Iter my_algo2(Iter p)
{
auto x = max2(Value_type<Iter>{}); // 适用于我们重载 max2() 的类型
// ...
}
25.4 建议
[1] 使用模板提高类型安全性;§25.1。
[2] 使用模板提高代码的抽象级别;§25.1。
[3] 使用模板提供灵活高效的类型和算法参数化;§25.1。
[4] 请记住,值模板参数必须是编译时常量;§25.2.2。
[5] 使用函数对象作为类型参数,通过“策略”参数化类型和算法;§25.2.3。
[6] 使用默认模板参数为简单用途提供简单符号;§25.2.5。
[7] 为不规则类型(如数组)特化模板;§25.3。
[8] 特化模板以针对重要情况进行优化;§25.3。
[9] 在任何特化之前定义主模板;§25.3.1.1。
[10] 特化必须在每种用途的作用域内;§25.3.1.1。
内容来源:
<<The C++ Programming Language >> 第4版,作者 Bjarne Stroustrup