【C++ Templates(15)】特化与重载
2018-05-30 本文已影响20人
downdemo
当泛型代码不再适用的时候
template<typename T>
class Array {
private:
T* data;
...
public:
Array(Array<T> const&);
Array<T>& operator= (Array<T> const&);
void exchangeWith (Array<T>* b) {
T* tmp = data;
data = b->data;
b->data = tmp;
}
T& operator[] (std::size_t k) {
return data[k];
}
...
};
template<typename T> inline
void exchange (T* a, T* b)
{
T tmp(*a);
*a = *b;
*b = tmp;
}
-
exchange()
对于简单类型可以轻松处理,但如果T是需要拷贝操作的类型,如Array<T>,则需要调用一次拷贝构造函数和两次拷贝赋值运算符,而exchange_with()
直接交换内部指针开销小得多
透明自定义(Transparent Customization)
- 使用新函数有一些不方便的地方
- Array类的用户要记住一个额外的接口并在适当情况下尽可能使用此接口
- 泛型算法通常不能区分各种不同的可能性
template<typename T>
void generic_algorithm(T* x, T* y)
{
...
exchange(x, y); // How do we select the right algorithm?
...
}
- 模板提供了透明自定义函数模板和类模板的方法解决此问题。对于函数模板通过重载来实现
template<typename T> inline
void quick_exchange(T* a, T* b) // (1)
{
T tmp(*a);
*a = *b;
*b = tmp;
}
template<typename T> inline
void quick_exchange(Array<T>* a, Array<T>* b) // (2)
{
a->exchange_with(b);
}
void demo(Array<int>* p1, Array<int>* p2)
{
int x = 42, y = -7;
quick_exchange(&x, &y); // uses (1)
quick_exchange(p1, p2); // uses (2)
}
- 对于第二个调用,虽然两个模板都能精确匹配,但重载会优先选择更加特殊的模板
语义的透明性(Semantic Transparency)
- 虽然上述两种交换的算法都能交换指针指向的值,但各自的side effects(副作用,预期作用之外的影响)截然不同
struct S {
int x;
} s1, s2;
void distinguish (Array<int> a1, Array<int> a2)
{
int* p = &a1[0];
int* q = &s1.x;
a1[0] = s1.x = 1;
a2[0] = s2.x = 2;
quick_exchange(&a1, &a2); // *p == 1 after this (still)
quick_exchange(&s1, &s2); // *q == 2 after this
}
- q仍指向s1,只是指针指向的值发生了改变。p原先是a1的data所在的位置,值为1,调用
quick_exchange()
后,a1和a2的data指针交换,但整个过程中p是不动的,*p仍为1,而a2的data到了p的位置,值为1,此时p和a2的data指向同一处。如果不明白参考下面代码
int main()
{
int* a = new int[3];
a[0] = 1;
int* p = &a[0]; // 之后a指向哪都不会改变p
int* b = new int[3];
int* tmp = b;
b = a; // 此时abp都指向一处
a = tmp; // a指向了原来b的那处,bp指向一处
cout << a[0] << endl; // 一个未定义的数
cout << b[0] << endl; // 1
cout << *p << endl; // 1
}
// 更简单的例子
int main()
{
int i = 1, j = 2;
int* p = &i;
int* q = p; // 之后p指向哪都不会改变q
p = &j;
cout << *q; // 1
}
- 原来的
exchange()
模板还可以对Array<T>进一步优化如下,使得不需要庞大的临时Array<T>对象。对这个模板递归调用,即使对Array<Array<char>>类型也能获得优化的性能。因为本身执行多个递归,所以不声明inline,原来的实现执行很少的操作,因此是内联的(但每个操作都很昂贵)
template<typename T>
void exchange (Array<T>* a, Array<T>* b)
{
T* p = &(*a)[0];
T* q = &(*b)[0];
for (std::size_t k = a->size(); k-- != 0; ) {
exchange(p++, q++);
}
}
重载函数模板
template<typename T>
int f(T)
{
return 1;
}
template<typename T>
int f(T*)
{
return 2;
}
int main()
{
std::cout << f<int*>((int*)nullptr) << std::endl; // 1
std::cout << f<int>((int*)nullptr) << std::endl; // 2
}
-
f<int*>
表明用int*
替换模板参数,对两个f模板,生成的重载集合包含两个函数f<int*>(int*)
和f<int*>(int**)
,实参类型是int*
,于是和第一个函数匹配。第二个调用同理
签名
- 只要具有不同的签名,两个函数就能同时存在于同一个程序中,函数签名定义如下(简单理解就是函数声明中所包含的信息)
- 非受限函数名称(或产生自函数模板的这类名称)
- 函数名称所属的类作用域或命名空间作用域,如果函数名称具有内部链接还包括该名称声明所在的编译单元
- 函数的const、volatile或const volatile限定符
- 函数的&或&&限定符
- 函数参数类型(如果是函数模板产生的函数,则参数类型是模板参数被替换前的类型)
- 如果函数产生自函数模板,则包括返回类型、模板参数和模板实参
- 原则上以下模板及其实例化体可以在一个程序中同时存在
template<typename T1, typename T2>
void f1(T1, T2);
template<typename T1, typename T2>
void f1(T2, T1);
template<typename T>
long f2(T);
template<typename T>
char f2(T);
- 但上述模板在同一作用域中声明,实例化过程可能导致二义性
#include <iostream>
template<typename T1, typename T2>
void f1(T1, T2)
{
std::cout << "f1(T1, T2)\n";
}
template<typename T1, typename T2>
void f1(T2, T1)
{
std::cout << "f1(T2, T1)\n";
}
int main()
{
f1<char, char>('a', 'b'); // 二义性错误
}
- 只有两个模板出现在不同的编译单元,两个实例化体才能在一个程序中同时存在
// Translation unit 1:
#include <iostream>
template<typename T1, typename T2>
void f1(T1, T2)
{
std::cout << "f1(T1, T2)\n";
}
void g()
{
f1<char, char>('a', 'b');
}
// Translation unit 2:
#include <iostream>
template<typename T1, typename T2>
void f1(T2, T1)
{
std::cout << "f1(T2, T1)\n";
}
extern void g(); // defined in translation unit 1
int main()
{
f1<char, char>('a', 'b');
g();
}
// output
f1(T2, T1)
f1(T1, T2)
重载的函数模板的局部排序
- 重新考虑前面这个例子,用给定的模板实参替换后,重载会选择最佳匹配,但即使没有显式提供模板实参,也会由模板实参推断来选中函数
#include <iostream>
template<typename T>
int f(T)
{
return 1;
}
template<typename T>
int f(T*)
{
return 2;
}
int main()
{
std::cout << f<int*>((int*)nullptr) << std::endl; // 1
std::cout << f<int>((int*)nullptr) << std::endl; // 2
// 下面的0被推断为int,匹配第一个模板
// 第二个模板不是候选函数,重载解析在此未发生
std::cout << f(0) << std::endl; // calls f<T>(T)
std::cout << f(nullptr); // calls f<T>(T)
// 下面对两个模板的实参推断都可以成功,匹配程度一样
// 但重载解析会选择产生自更特殊的第二个模板的函数
// 因为第一个模板适用任何类型实参,第二个只适用指针类型实参
std::cout << f((int*)nullptr); // calls f<T>(T*)
}
正式的排序原则
- 比较两个同名的函数模板ft1和ft2,不考虑未被使用的默认实参和省略号参数,通过如下替换为两个模板分别虚构两份不同的实参类型列表
- 用唯一的虚构类型替换每个模板类型参数
- 用唯一的虚构类型替换每个模板的模板参数
- 用唯一的适当类型虚构值替换每个非类型参数
- 如果第二个模板对第一份列表可以进行实参推断(进行精确匹配),而第一个模板对第二份列表不能,则第一个模板更特殊,反之则反。如果两个同时推断成功或失败,则不存在特殊的排序关系
- 对于上例,虚构两个实参类型列表(A1)和(A2),第一个模板用A2替换T就可以推断第二份列表,反过来则不行,因此第二个模板更特殊。再考虑一个更复杂的例子
template<typename T>
void t(T*, T const* = nullptr, ...);
template<typename T>
void t(T const*, T*, T* = nullptr);
void example(int* p)
{
t(p, p); // 错误:二义性调用
}
- 这里省略号参数和默认实参都没被使用,排序时不会考虑。虚构的参数列表是
(A1*, A1 const*)
和(A2 const*, A2*)
,对第二个模板用A1 const
替换T就可以对(A1*, A1 const*)
进行实参推断,但不是精确匹配,因为用(A1*, A1 const*)
类型实参调用t<A1 const>(A1 const*, A1 const*, A1 const* = 0)
时要调整const限定符。第一个模板对(A2 const*, A2*)
也不能精确匹配,因此这两个模板没有排序关系,调用是二义性的
模板和非模板
- 函数模板也可以和非模板函数重载,其他条件相同时优先调用非模板函数
#include <string>
#include <iostream>
template<typename T>
std::string f(T)
{
return "Template";
}
std::string f(int&)
{
return "Nontemplate";
}
int main()
{
int x = 7;
std::cout << f(x) << '\n'; // prints: Nontemplate
}
- 当有const和引用限定符时重载解析会改变
#include <string>
#include <iostream>
template<typename T>
std::string f(T&)
{
return "Template";
}
std::string f(int const&)
{
return "Nontemplate";
}
int main()
{
int x = 7;
std::cout << f(x) << '\n'; // prints: Template
int const c = 7;
std::cout << f(c) << '\n'; // prints: Nontemplate
}
- 因此通常声明如下是一个好的做法
template<typename T>
std::string f(T const&)
{
return "Template";
}
- 然而这有时也会造成意外的错误
#include <string>
#include <iostream>
class C {
public:
C() = default;
C (C const&) {
std::cout << "copy constructor\n";
}
C (C&&) {
std::cout << "move constructor\n";
}
template<typename T>
C (T&&) {
std::cout << "template constructor\n";
}
};
int main()
{
C x;
C x2{x}; // prints: template constructor
C x3{std::move(x)}; // prints: move constructor
C const c;
C x4{c}; // prints: copy constructor
C x5{std::move(c)}; // prints: template constructor
}
可变参数函数模板
#include <iostream>
template<typename T>
int f(T*)
{
return 1;
}
template<typename... Ts>
int f(Ts...)
{
return 2;
}
template<typename... Ts>
int f(Ts*...)
{
return 3;
}
int main()
{
std::cout << f(0, 0.0); // calls f<>(Ts...)
std::cout << f((int*)nullptr, (double*)nullptr); // calls f<>(Ts*...)
std::cout << f((int*)nullptr); // calls f<>(T*)
}
- 包扩展同理
#include <iostream>
template<typename... Ts> class Tuple
{
};
template<typename T>
int f(Tuple<T*>)
{
return 1;
}
template<typename... Ts>
int f(Tuple<Ts...>)
{
return 2;
}
template<typename... Ts>
int f(Tuple<Ts*...>)
{
return 3;
}
int main()
{
std::cout << f(Tuple<int, double>()); // calls f<>(Tuple<Ts...>)
std::cout << f(Tuple<int*, double*>()); // calls f<>(Tuple<Ts*...>)
std::cout << f(Tuple<int*>()); // calls f<>(Tuple<T*>)
}
显式特化
- 显式特化是一种语言特性,通常也称为全局特化,一般全局特化和局部特化都是显式的,所以讨论中一般不使用显式特化这个概念。全局特化和局部特化没有引入一个新模板或实例,只是对原来在泛型(非特化)模板中已经隐式声明的实例提供另一种定义
- 全局特化为模板提供了一种模板参数可以被全局替换的实现
- 类模板不能被重载,但类模板(及其成员)和函数模板都可以全局特化
全局的类模板特化
template<typename T>
class S {
public:
void info() {
std::cout << "generic (S<T>::info())\n";
}
};
template<>
class S<void> {
public:
void msg() {
std::cout << "fully specialized (S<void>::msg())\n";
}
};
- 全局特化的实现并不需要与泛型实现有任何关联,事实上全局特化只和类模板的名称有关联
- 指定的模板实参列表必须对应模板参数列表,比如不能用非类型值替换模板类型参数。如果模板参数有默认实参,用来替换的模板实参就是可选的
template<typename T>
class Types {
public:
using I = int;
};
template<typename T, typename U = typename Types<T>::I>
class S; // (1)
template<>
class S<void> { // (2)
public:
void f();
};
template<> class S<char, char>; // (3)
template<> class S<char, 0>; // ERROR: 0 cannot substitute U
int main()
{
S<int>* pi; // OK: uses (1), no definition needed
S<int> e1; // ERROR: uses (1), but no definition available
S<void>* pv; // OK: uses (2)
S<void, int> sv; // OK: uses (2), definition available
S<void, char> e2; // ERROR: uses (1), but no definition available
S<char, char> e3; // ERROR: uses (3), but no definition available
}
template<>
class S<char, char> { // definition for (3)
};
- 全局特化的声明不一定是定义,声明之后,对该特化的模板实参列表的调用不使用模板的泛型定义,而是使用全局特化的定义,如果没有提供这个定义则会出错
- 前置声明有时很有用,这样可以构造相互依赖的类型。特化声明不是模板声明,所以定义时应该使用普通的定义语法(即不指定template<>前缀)
template<typename T>
class S;
template<> class S<char**> {
public:
void print() const;
};
// 下面的定义不能使用template<>前缀
void S<char**>::print()
{
std::cout << "pointer to pointer to char\n";
}
- 更复杂的例子
template<typename T>
class Outside {
public:
template<typename U>
class Inside {
};
};
template<>
class Outside<void> {
// 下面的嵌套类和上面定义的泛型模板之间并不存在联系
template<typename U>
class Inside {
private:
static int count;
};
};
// 下面的定义不能使用template<>前缀
template<typename U>
int Outside<void>::Inside<U>::count = 1;
- 可以用全局模板特化代替对应的泛型模板的某个实例化体,但全局特化和模板生成的实例化版本不能在一个程序中同时存在,否则会发生编译期错误
template <typename T>
class Invalid {
};
Invalid<double> x1; // 产生一个Invalid<double>实例化体
template<>
class Invalid<double>; // 错误:Invalid<double>已经被实例化了
- 如果在不同的编译单元出现这种情况很难捕捉错误,如果没有特殊目的应该避免让模板特化来源于外部资源。下面是一个无效的例子
// Translation unit 1:
template<typename T>
class Danger {
public:
enum { max = 10; };
};
char buffer[Danger<void>::max]; // uses generic value
extern void clear(char*);
int main()
{
clear(buffer);
}
// Translation unit 2:
template<typename T>
class Danger;
template<>
class Danger<void> {
public:
enum { max = 100; };
};
void clear(char const* buf)
{
// 可能与原先定义的数组大小不匹配
for(int k=0; k < Danger<void>::max; ++k) {
buf[k] = '\0';
}
}
全局的函数模板特化
- 全局函数模板特化和类模板特化的区别是,函数模板特化引入了重载和实参推断,如果能通过实参推断确定模板的特殊化版本,全局特化就可以不声明显式的模板实参
template<typename T>
int f(T) // (1)
{
return 1;
}
template<typename T>
int f(T*) // (2)
{
return 2;
}
template<> int f(int) // OK: specialization of (1)
{
return 3;
}
template<> int f(int*) // OK: specialization of (2)
{
return 4;
}
- 全局函数模板特化不能包含默认实参,但显式特化可以应用要被特化的模板所指定的默认实参
template<typename T>
int f(T, T x = 42)
{
return x;
}
template<> int f(int, int = 35) // ERROR!
{
return 0;
}
template<typename T>
int g(T, T x = 42)
{
return x;
}
template<> int g(int, int y)
{
return y/2;
}
int main()
{
std::cout << f(0) << std::endl; // should print 21
}
- 全局特化声明的声明对象不是一个模板,非内联的全局函数模板特化在同个程序中的定义只能出现一次。必须确保全局函数模板特化的声明紧跟在模板定义之后,以避免试图使用一个由模板直接生成的函数,前面的例子中通常应该把模板g的声明放在两个文件中
#ifndef TEMPLATE_G_HPP
#define TEMPLATE_G_HPP
// 模板定义应放在头文件中
template<typename T>
int g(T, T x = 42)
{
return x;
}
// 特化声明禁止模板进行实例化,但为避免重复不能在此定义
template<> int g(int, int y);
#endif // TEMPLATE_G_HPP
// 实现文件
#include "template_g.hpp"
template<> int g(int, int y)
{
return y/2;
}
- 另一种方法是把特化声明为内联函数,这样该函数的定义就可以放在头文件中
全局变量模板特化
template<typename T> constexpr std::size_t SZ = sizeof(T);
template<> constexpr std::size_t SZ<void> = 0;
- 变量模板特化不要求有一个类型匹配特化的模板
template<typename T> typename T::iterator null_iterator;
template<> BitIterator null_iterator<std::bitset<100>>;
// BitIterator doesn't match T::iterator, and that is fine
全局成员特化
- 除了成员模板,类模板的成员函数和普通的静态成员变量也可以全局特化,语法要求在每个外围类模板加上template<>前缀。如果要对成员模板特化也要加上另一个template<>前缀
template<typename T>
class Outer { // (1)
public:
template<typename U>
class Inner { // (2)
private:
static int count; // (3)
};
static int code; // (4)
void print() const { // (5)
std::cout << "generic";
}
};
template<typename T>
int Outer<T>::code = 6; // (6)
template<typename T> template<typename U>
int Outer<T>::Inner<U>::count = 7; // (7)
template<>
class Outer<bool> { // (8)
public:
template<typename U>
class Inner { // (9)
private:
static int count; // (10)
};
void print() const { // (11)
}
};
- (1)的(4)(5)成员code和print都具有一个外围类模板,因此需要一个template<>前缀说明,后面用一个模板实参集来全局特化。特化的定义会替代(4)(5)的泛型定义,但类
Outer<void>
的其他成员仍默认产自(1)的模板,另外经过下列定义后就不能再次提供Outer<void>
的显式特化
template<>
int Outer<void>::code = 12;
template<>
void Outer<void>::print() const
{
std::cout << "Outer<void>";
}
- 类似于全局函数模板特化,为了避免多处定义需要一种不定义但声明类模板普通成员特化的方法。普通类的成员函数和静态成员变量在类外的非定义声明是非法的,但类模板的特化成员该声明合法,即上述定义可以有如下声明
template<>
int Outer<void>::code;
template<>
void Outer<void>::print() const;
- 如果静态成员变量的类型是一个只能用默认构造函数初始化的类型,就不能提供它的全局特化定义
class DefaultInitOnly {
public:
DefaultInitOnly() = default;
DefaultInitOnly(DefaultInitOnly const&) = delete;
};
template<typename T>
class Statics {
private:
static T sm;
};
// 下面只是一个声明
template<>
DefaultInitOnly Statics<DefaultInitOnly>::sm;
// 下面是定义(C++11前不存在定义的方法)
template<>
DefaultInitOnly Statics<DefaultInitOnly>::sm{};
- 对于成员模板
Outer<T>::Inner
也能用给定的模板实参特化,且不影响Outer<T>
实例化其他成员。由于存在外围模板Outer<T>
,所以需要添加一个template<>前缀
template<>
template<typename X>
class Outer<wchar_t>::Inner {
public:
static long count; // 成员类型发生了改变
};
template<>
template<typename X>
long Outer<wchar_t>::Inner<X>::count;
- 模板
Outer<T>::Inner
也可以全局特化,但只能针对Outer<T>
的某个给定实例,且需要添加两个template<>前缀(外围类和全局特化的内围模板各需要一个)
template<>
template<>
class Outer<char>::Inner<wchar_t> {
public:
enum { count = 1; };
};
// 下列代码不合法:template<>不能位于模板实参列表后面
template<typename X>
template<> class Outer<X>::Inner<void>; // ERROR!
-
Outer<bool>
已经在前面全局特化了,成员模板不存在外围模板,因此只需要一个template<>前缀
template<>
class Outer<bool>::Inner<wchar_t> {
public:
enum { count = 2; };
};
局部的类模板特化
template<typename T>
class List { // (1)
public:
...
void append(T const&);
inline std::size_t length() const;
...
};
- 对使用这个链表模板的大项目,可能实例化多种类型的模板成员,对没有内联扩展的成员函数
List<T>::append()
就可能明显增加代码大小。从低层看,List<int*>::append()
和List<void*>::append()
的代码是完全相同的,因此希望所有的List of pointers共享一个实现
template<typename T>
class List<T*> { // (2)
private:
List<void*> impl;
...
public:
...
inline void append(T* p) {
impl.append(p);
}
inline std::size_t length() const {
return impl.length();
}
...
};
- 原来的(1)称为基本模板,而后的定义称为局部特化。局部特化的语法是一个模板参数列表声明(即
template<...>
)和在类模板名称后显式指定的模板实参列表(这里是<T*>) - 但上面的代码存在一个问题,
List<void*>
会递归地包含一个相同类型的List<void*>
成员,产生无限递归,解决方法是在局部特化前提供一个全局特化,匹配时全局特化会优于局部特化,指定List的所有成员函数都被委托给List<void*>
的实现(通过容易内联的函数),这就是克服模板代码膨胀的有效方法之一
template<>
class List<void*> { // (3)
...
void append (void* p);
inline size_t length() const;
...
};
- 对于局部特化声明的参数列表和实参列表存在一些重要约束
- 局部特化的实参必须和基本模板的相应参数种类(类型、非类型或模板)匹配
- 局部特化的参数列表不能有默认实参,但局部特化可以使用基本类模板的默认实参
- 局部特化的非类型实参只能是非类型值或普通的非类型模板参数,不能是依赖型表达式(如2*N,N是模板参数)
- 局部特化的模板实参列表不能和基本模板的参数列表完全等同(不考虑重新命名)
- 如果一个模板实参是包扩展,必须出现在模板实参列表末尾
template<typename T, int I = 3>
class S; // 基本模板
template<typename T>
class S<int, T>; // 错误:参数类型不匹配
template<typename T = int>
class S<T, 10>; // 错误:不能有默认实参
template<int I>
class S<int, I*2>; // 错误:不能有非类型表达式
template<typename U, int K>
class S<U, K>; // 错误:局部特化和基本模板没有本质区别
template<typename... Ts>
class Tuple;
template<typename Tail, typename... Ts>
class Tuple<Ts..., Tail>; // 错误:包扩展不在末尾
template<typename Tail, typename... Ts>
class Tuple<Tuple<Ts...>, Tail>; // OK:包扩展在嵌套模板实参列表末尾
- 每个局部特化都会关联基本模板,使用模板时编译器会查找基本模板,接着匹配调用实参和相关特化的实参,再确定使用哪个模板。如果能找到多个匹配的特化,选择最特殊的特化,如果没有最特殊的则产生二义性错误
- 类模板局部特化的参数个数可以和基本模板不一致
template<typename C>
class List<void* C::*> { // (4)
public:
// partial specialization for any pointer-to-void* member
// every other pointer-to-member-pointer type will use this
using ElementType = void* C::*;
...
void append(ElementType pm);
inline std::size_t length() const;
...
};
template<typename T, typename C>
class List<T* C::*> { // (5)
private:
List<void* C::*> impl;
...
public:
// partial specialization for any pointer-to-member-pointer type
// except pointer-to-void* member which is handled earlier;
// note that this partial specialization has two template parameters,
// whereas the primary template only has one parameter
using ElementType = T* C::*;
...
void append(ElementType pm) {
impl.append((void* C::*)pm);
}
inline std::size_t length() const {
return impl.length();
}
...
};
- 除了模板参数数量不同,(4)定义的公共实现本身也是一个局部特化,而其他的局部特化,即(5),都是把实现委托给这个公共实现,显然(4)的公共实现比(5)特殊,因此不会出现二义性
- 显式写入的模板参数数量可以与基本模板的不同,这可以发生于使用默认模板实参,或可变参数模板
template<typename... Elements>
class Tuple; // primary template
template<typename T1>
class Tuple<T>; // one-element tuple
template<typename T1, typename T2, typename... Rest>
class Tuple<T1, T2, Rest...>; // tuple with two or more elements
局部的变量模板特化
- 语法与全局的变量模板特化类似,除了template<>被一个具体的模板声明头替换,且模板实参列表必须依赖于模板参数
template<typename T> constexpr std::size_t SZ = sizeof(T);
template<typename T> constexpr std::size_t SZ<T&> = sizeof(void*);
- 和变量模板的全局特化一样,局部特化的类型不需要和基本模板的匹配
template<typename T> typename T::iterator null_iterator;
template<typename T, std::size_t N>
T* null_iterator<T[N]> = null_ptr;
// T* doesn't match T::iterator, and that is fine