C++模板基础

前言

一些关于模板的零碎知识,容易被忽略的知识,来源是《C++ templates》第8章。

参数化声明

C++现今支持两种基本类型的模板:类模板和函数模板,这个分类实际上还包括成员模板。这些模板的声明和普通类与普通函数的声明很相似,唯一的区别是模板声明需要引入一个参数化字句,字句的格式大体如下:

1
2
//export为可选的
[export] template<...parameters here...>

另外,如果要有多个字句(例如类模板中包含有成员模板),字句的顺序是从最外围的类模板开始,依次到达内部模板。

此外,联合(Union)模板也是允许的。

1. 虚成员函数

成员函数模板不能被声明成为虚函数。因为函数模板实例化个数不定,而虚函数的实现一般是一个确定大小的表。

2. 模板的链接

每个模板都必须有一个名字,而且在它所属的作用域下,改名字是唯一的;除非函数模板可以被重载。特别是,类模板不能和另外一个实体共享一个名字,这一点和class类型是不同的:

1
2
3
4
5
6
7
8
9
10
11
int C;
class C; //正确:类名称和非类名称位于不同的名字空间(space)

int X;
template<typename T>
class X; //错误:和变量X冲突


struct S;
template <typename T>
class S; //错误:和struct S冲突

模板名字是具有链接的,但他们不能具有C链接。但我们在大多数情况下所说的是标准的链接,同时也存在非标准的链接,它们可以具有一个依赖于实现的函数。见下面例子所示:

1
2
3
4
5
6
7
8
9
extern "C++" template <typename T>
void normal(); //这是缺省情况,上面的链接规范可以不写

extern "C" template <typename T>
void invalid(); //错误的:模板不能具有C链接

extern "Xroma" template <typename T>
void xroma_link(); //非标准的,但某些编译器将来可能支持
//Xroma语言的链接兼容性

模板通常具有外部链接。唯一的例外就是前面具有static修饰符的名字空间作用域下的函数模板:

1
2
3
4
5
template <typename T>
void external(); //作为一个声明,引用位于其他文件的,具有相同名称的实体:
//即引用位于其他文件的external()函数模板,也称前置声明
template <typename T>
static void A(); //与其他文件中具有相同名称的模板没有关系,即不是外部链接

3. 基本模板

如果模板声明的是一个普通声明,我们就称它声明的是一个基本模板。这类模板声明是指:没有在模板名称后面添加一对尖括号(和里面实参)的声明。

1
2
3
4
5
template <typename T> class Box;	//正确,基本模板
template <typename T> class Box<T>; //错误

template <typename T> void translate(T*); //正确:基本模板
template <typename T> void translate<T>(T*); //错误

声明局部特化的时候,声明的就是非基本模板.


模板参数

现今存在3中模板参数:

  1. 类型参数
  2. 非类型参数
  3. 模板的模板参数

1. 类型参数

类型参数是通过关键字typename或者class引入的: 他们两个几乎是等同的.关键字后面必须是一个简单的标识符, 后面用逗号隔开下一个参数声明. 等号( == )代表接下来的是缺省实参, 一个封闭的尖括号 ( > )表示参数化字句的结束.

在模板声明内部, 类型参数的作用类似于typedef名称. 例如, 如果T是一个模板参数, 就不能使用诸如class T等形式的修饰名称, 即使T是一个class类型替换的参数也不行.

1
2
3
4
5
6
template <typename Allocator>
class List{
class Allocator* allocator; //错误
friend class Allocator; //错误
...
};

2. 非类型参数

非类型参数表示的是: 在编译期或链接期可以确定的常值.这种参数的类型必须是下面的一种:

  • 整型或者枚举类型
  • 指针类型( 包含普通对象的指针类型、函数指针类型、指向成员的指针类型 )
  • 引用类型(指向对象或者指向函数的引用都是允许的)

函数和数组类型也可以被指定为非模板参数,但要把他们先隐式转换为指针类型,这种类型转换也成为decay:

1
2
template <int buf[5]> class Lexer;	//buf实际上是一个int*类型
template <int* buf> class Lexer; //正确:这是上面的重新声明。

非类型模板参数的声明和变量的声明很相似,但他们不能具有static、mutable等修饰符:只能具有const和volatile限定符。但如果这两个限定符限定的如果是最外层的参数类型,编译器会将他们忽略:

1
2
template <int const length> class Buffer;	//这里的const是没用的,被忽略了
template <int length> class Buffer; //和上面是等同的

3. 模板的模板参数

模板的模板参数是代表类模板的占位符。它的声明和类模板的声明很类似,但不能使用关键字struct和union:

1
2
3
4
5
6
7
8
template <template<typename X> class C>		//正确
void f(C<int>* p);

template <template <typename X> struct C> //错误
void f(C<int>* p);

template <template <typename X> union C> //错误
void f(C<int>* p)

模板的模板参数的参数(如下面的A)可以具有缺省模板实参。显然,只有在调用时没有指定改参数的情况下,才会应用缺省模板实参:

1
2
3
4
5
6
template <template<typename T,
typename A = MyAllocator> class Container>
class Adaptation{
Container<int> storage;//隐式等同于Container<int, MyAllocator>
...
};

对于模板的模板参数而言,它的参数名称只能被自身其他参数的声明使用。下面的假设例子说明了这一点:

1
2
3
4
5
6
7
8
9
10
11
12
template <template <typename T, T*> class Buf>
class Lexer{
static char storage[5];
Buf<char, &Lexer<Buf>::storage[0]> buf;
...
};

template <template <typename T> class Buf>
class Lexer{
static T* storage; //错误:模板的模板参数在这里不能被使用
...
};

通常而言,模板的模板参数的参数名称(如上面例子的T)并不会在后面被用到。因此,该参数也经常被省略不写,即没有命名。例如,前面Adaptation模板的例子可以这样声明:

1
2
3
4
5
template<template <typename, typename = MyAllocator> class Container>
class Adaptation
{
Container<int> storage;
};

4. 缺省模板实参

现今只有模板声明才能具有缺省模板实参。任何类型的模板参数都可以同游一个缺省实参,只要该实参能够匹配这个参数就可以。显然,缺省实参不能依赖于自身的参数;但亦可依赖于前面的参数:

1
2
3
template <typename T, typename Allocator = allocator<T>>
class List;
//就是说,allocator<T>不能依赖于本身参数Allocator,但是能依赖于前面参数T

与缺省的函数调用参数的约束一样,对于任一个模板参数,只有在之后的模板参数都提供了缺省实参的前提下,才能具有缺省模板实参,后面的缺省值通常是在某个模板声明中提供的,但也可以在前面的模板声明中提供。下面的例子说明了这一点:

1
2
3
4
5
6
7
8
9
10
11
template <typename T1, typename T2, typename T3,
typename T4 = char, typename T5 = char>
class Quintuple; //正确

template <typename T1, typename T2, typename T3 = char,
typename T4, typename T5>
class Quintuple; //正确,根据前面的模板声明,T4和T5已经具有缺省值了

template <typename T1 = char, typename T2, typename T3,
typename T4, typename T5>
class Quintuple; //错误,T1不能具有缺省实参,因为T2还没有缺省实参

另外,缺省实参不能重复声明:

1
2
3
4
5
template <typename T = void>
class Value;

template <typename T = void>
class Value; //错误:重复出现缺省实参

模板实参

模板实参是指:在实例化模板时,用来替换模板参数的值。我们可以使用下面几种不同的机制来确定这些值:

  • 显式模板实参:紧跟在模板名称后面,在一对尖括号内部的显式模板实参值。所组成的整个实体称为template-id。
  • 注入式(injected)类名称:对于具有模板参数P1、P2……的类模板X,在它的作用域中,模板名称(即X)等同于template-id:X<P1, P2, ……>。
  • 缺省模板实参:如果提供缺省模板实参的话,在类模板的实例中就可以省略显式模板实参。然而,即使所有的模板参数都具有缺省值,一对尖括号还是不能省略的(即使尖括号内部为空,也要保留尖括号)。
  • 实参演绎,对于不是显式指定的函数模板实参,可以在函数的调用语句中,根据函数调用实参的类型来演绎出函数模板实参。事实上,实参演绎还可以在其他几种情况下出现。另外,如果所有的模板实参都可以通过演绎获得,那么在函数模板名称后面就不需要指定尖括号。

1. 函数模板实参

对于函数模板的模板实参,我们可以显式指定他们,或者借助于模板的使用方式对他们进行实参演绎。然而,某些模板实参永远也得不到演绎的机会,于是,我们最好把这些实参所对应的参数放在模板参数列表的开始处,从而可以显式指定这些参数,而其他的参数仍然可以进行实参演绎。例如:

1
2
3
4
5
6
7
8
9
10
11
12
template <typename DstT, typename SrcT>
inline DstT implicit_cast(SrcT const& x)
{ //SrcT可以被演绎,但是DstT不可以
return x;
}

int main()
{
max<double>(1.0, -3.0); //显式指定模板实参
max(1.0, -3.0); //模板实参被隐式演绎成double
max<int>(1.0, 3.0); //显式的<int>禁止了演绎,因此返回结果时int类型
}

由于函数模板可以被重载,所以对于函数模板而言,显式提供所有的实参并不足以标识每一个函数:在一些例子中,它标识的是由许多函数组成的函数集合。下面的例子清楚地说明了这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename Func, typename T>
void apply(Func func_ptr, T t)
{
func_ptr(x);
}

template <typename T> void single(T);
template <typename T> void multi(T);
template <typename T> void multi(T*);

int main()
{
apply(&single<int>, 1); //正确
apply(&multi<int>, 1); //错误:&multi<int>不唯一
}

在这个例子中,apply()的第一次调用时正确的,因为表达式&singal<int>的类型时正确的,因此,可以很容易地演绎出Func参数地模板实参值。然而,在第2此调用中,&multi<int>可以是两种函数类型中的任意一种,因此,在这种情况下会产生二义性,不能演绎出Func地实参。

另外,在函数模板中,显式指定模板实参可能会试图构造一个无效的C++类型。考虑下面地重载模板函数:

1
2
template <typename T> RT1 test(typename T::X const*);
template <typename T> RT2 test(...);

表达式test<int>可能会使第一个函数模板毫无意义,因为基本int类型根被就没有成员类型X。然而,第二个模板就没有这种问题。因此,表达式&test<int>能够标识一个唯一函数地地址(即第二个函数的地址)。而且,不能用int来替换第一个模板的参数,并不意味着&test<int>使非法的(就是下面的SFINAE原则)。实际上&test<int>在这里是有效的,也是合法的。

显然,“替换失败并非错误(subsitution-failure-is-not-an-error,SFINAE)”原则是令函数模板可以重载的重要因素。然而,它同时也涉及到值得我们注意的编译期技术。例如,假设类型RT1和RT2的定义如下:

1
2
typedef char RT1;
typedef struct { char a[2]; } RT2;

于是,我们就可以在编译器奸杀(也就是说,检查是否可以把它看成一个constant-expression)给定类型T是否具备成员类型X:

1
#define type_has_member_type_X(T) (sizeof(test<T>(0)) == 1)

SFINAE原则保护的只是:允许试图创建无效的类型。但并不允许试图计算无效的表达式。因此,下面的例子是错误的C++例子:

1
2
3
4
5
6
7
template <int I> viud f(int (&)[24/(4-I)]);
template <int I> viud f(int (&)[24/(4+I)]);

int main()
{
&f<4>; //错误:替换后第一个除数等于0(不能应用SFINAE)
}

即使第2个模板支持这种替换,它的除数也不会为0,但是这个例子是错误的。而且,这种错误指挥在表达式自身体现,并不会在模板参数表达式的绑定中体现。因此,下面的例子是合法的:

1
2
3
4
5
6
7
template <int N>int g() { return N; }
template <int* p> int g() {return *P;}

int main()
{
return g<1>(); //虽然数字1不能被绑定到int* 参数,但是应用了SFINAE原则
}

2. 类型实参

模板的类型实参是一些用来指定模板类型参数的值。我们平时使用的大多数类型都可以被用作模板的类型实参,但有两种情况除外:

  1. 局部类和局部枚举(换句话说,指在函数定义内部声明的类型)不能作为模板的类型实参。
  2. 未命名的class类型或者未命名的枚举类型不能作为模板的类型实参(然而,通过typedef声明给出的未命名类和枚举是可以作为模板类型实参的)。

3. 非类型实参

非类型模板实参是那些替换非类型参数的值。这个值必须是以下几种中的一种:

  • 某一个具有正确类型的非类型模板参数
  • 一个编译器整型常值(或枚举值)。这只有在参数类型和值的类型能够进行匹配,或者值的类型可以隐式地转换为参数类型(例如,一个char值可以作为int参数地实参)的前提下,才是合法的。
  • 前面有单目运算符&(即取址)的外部变量或者函数的名称。对于函数或数组变量,&运算符可以省略。这类模板实参可以匹配指针类型的非类型参数。
  • 对于引用类型的非类型模板参数,前面没有&运算符的外部变量和外部函数也是可取的。
  • 一个直线成员的指针常量。换句话说,类似&C::m的表达式,其中C是一个class类型,m是一个非静态成员(成员变量或者函数)。这类实参只能匹配类型为”成员指针“的非类型参数。

当实参匹配”指针类型或者引用类型的参数“时,用户定义的类型转换(例如单参数的构造函数和重载类型转换运算符)和由派生类到积累的类型转换,都是不会被考虑的,即使在其他情况下,这些隐式类型转换是有效的,但在这里都是无效的。隐式类型转换的唯一应用只是:给实参加上关键字const或者volatile。

下面是一些有效的非类型模板实参地例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
template<typename T, T nontype_param>
class C;

C<int, 33>* c1; //整型

int a;
C<int*, &a>* c2; //外部变量地址

void f();
void f(int);
C<void (*)(int), f>* c3; //函数名称:在这个例子中,重载解析会选择f(int)

class X{
public:
int n;
static bool b;
};

C<bool&, X::b>* c4; //静态类成员是可取地。变量(和函数)名称

C<int X::*, &X::n>* c5; //指向成员地指针常量

template <typename T>
void temp_func();

C<void (), &temp_func<double>>* c6; //函数模板实例同时也是函数

模板实参地一个普遍约束是:在程序创建地时候,编译器或者链接器要能够确定实参地值。如果实参的值要等到程序运行时才能确定(譬如,局部变量的地址),就不符合”模板是在程序创建的时候进行实例化“的概念了。

另一方面,有些常量不能作为有效的非类型实参,这也许会令你觉得差异,这些常量值包括:

  • 空指针常量
  • 浮点型值
  • 字符串

有关字符串的一个问题就是:两个完全等同的字符串可以存储在两个不同的地址中。在此,我们用一种(很笨的)解决方法来表达需要基于字符串进行实例化的模板:引入一个额外的变量来存储这个字符串。

1
2
3
4
5
6
template <char const* str>
class Message;

extern char const hello[] = "hello world!";

Message<hello> *hello_msg;

可以看到,我们使用了关键字extern。因为如果不适用这个关键字,上面的const数组变量将具有内部链接。

下面给出一些错误例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T, T nontype_param>
class C;

class Base{
public:
int i;
}base;

class Derived: public Base{
}derived_obj;

C<Base*, &derived_obj>* err1; //错误,这里不会考虑派生类到基类的类型转换

C<int&, base.i>* err2; //错误:域运算符(.)后面的变量不会被看成变量

int a[10];
C<int*, &a[0]>* err; //错误:不能使用数组某一元素的地址

4. 模板的模板实参

”模板的模板实参“必须是一个类模板,它本身具有参数,该参数必须精确匹配它“所替换的模板的模板参数”本身的参数。在匹配过程中,”模板的模板实参“的缺省模板实参将不会被考虑(但是如果”模板的模板参数“具有缺省实参,那么模板的实例化过程时会考虑模板的模板参数的缺省实参的)。

从语法上讲,只有关键字class才能被用来声明模板的模板参数;但是这并不意味只有用关键字class声明的类模板才能作为它的替换实参。

5. 实参的等价性

当每个对应实参值都相等时,我们就称这两组模板实参时相等的。对于类型实参,typedef名称并不会对等价性产生影响:就是说,最后比较的还是typedef原本的类型。对于非类型的整型实参,进行比较的时实参的值;至于这些值时如何表达的,也不会产生影响,下面的例子说明了这一点:

1
2
3
4
5
6
7
template <typename T, int I>
class Mix;

typedef int Int;

Mix<int, 3*3>* p1;
Mix<Int, 4+5>* p2; //p2和p1的类型时相同的

另外,从函数模板产生(即实例化出来的)函数一定不会等于普通函数,即使这两个函数具有相同的类型和名称。这样,针对类成员,我们可以引申出两点结论:

  • 从成员函数模板产生的类永远也不会改写一个虚函数(进一步说明成员函数模板不能是一个虚函数)。
  • 从构造函数模板产生的构造函数一定不会是缺省的拷贝构造函数。

友元

友元声明的基本概念时简单的:授予”某个类或者函数访问友元声明所在的类“的权利。然而,由于以下两个事实,这些简单概念却变得有些复杂:

  1. 友元声明可能是某个实体的唯一声明。
  2. 友元函数的声明可以是一个定义。

友元类的声明不能是类定义,因此友元类通常都不会出现问题。在引入模板之后,友元类声明的唯一变化只是:可以命名一个特定的类模板实例为友元。

显然,如果要把类模板的实例声明为其他类(或者类模板)的友元,该类模板在声明的地方必须是可见的。然而,对于一个普通类,就没有这个要求

1
2
3
4
5
template <typename T>
class Tree{
friend class Factory; //OK,即使这是Factory的首次声明
friend class Node<T>; //ERROR(如果此前从未声明过Node)
};

1. 友元函数

通过确认紧接在友元函数名称后面的是一对尖括号,我们可以把函数模板的实例声明为友元。尖括号可以包含模板实参,但也可以通过调用参数来演绎出实参。如果全部实参都能获得演绎的话,那么尖括号里面可以为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T1, typename T2>
void combine(T1, T2);

class Mixer{
friend void combine<>(int&, int&);
//正确
friend void combine<int, int>(int, int);
//正确
friend void combine<char>(char, int);
//正确
friend void combine<char>(char& int);
//错误
frined void combine<>(long, long) { ... }
//错误,这里的友元声明不允许出现定义
}

不能在友元声明中定义一个模板实例(我们最多只能定义一个特化);因此,命名一个实例的友元声明是不能作为定义的。

如果名称 后面没有紧跟一堆尖括号,那么只有在下面两种请胯下是合法的:

  1. 如果名称不是受限的(就是说,没有包含一个形如双冒号的域运算符),那么该名称一定不是(也不能)引用一个模板实例。如果在友元声明的地方,还看不多所普配的非模板函数,那么这个友元声明就是函数的首次声明。于是,该声明可以是定义
  2. 如果名称是受限的(就是说前面有双冒号),那么该名称必须引用一个在此之前声明的函数或者函数模板。在匹配过程中,匹配的函数要优先于匹配的函数模板。然而,这样的友元声明不能是定义

下面的例子可以说明这些情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void miltiply(void*);	//普通函数

template <typename T>
void multiply(T); //函数模板

class Comrades{
friend void multiply(int) { }
//定义了一个新的函数::multiply(int)非受限函数名称,不能引用模板实例
friend void ::multiply(void*);
//引用上面的普通函数,不会引用multiply<void*>实例
friend void ::multiply(int);
//引用一个模板实例
friend void ::multiply<double*>(double*);
//首秀按名称还可以具有一对尖括号,但模板在此必须是可见的
friend void ::error() {}
//错误:受限的友元不能是一个定义
};

在前面的例子中,我们是在一个普通类里面声明友元函数。如果需要在类模板里面声明友元函数,前面的这些规则仍然是适用的,唯一的区别就是:可以使用模板参数来标识友元函数。

然而,如果我们在类模板中定义一个友元函数,那么将会出现一个很有趣的现象。因为对于任何只在模板内部声明的实体,都要等到模板被实例化之后,才会使一个具体的实体:在这之前该实体是不存在的。类模板的友元函数也是如此。考虑下面的例子:

1
2
3
4
5
6
7
8
9
template <typename T>
class Creator{
friend void appear(){
... //一个新函数::appear(),但要等到Creator被实例化之后才存在
}
};

Creator<void> miracle; //这时才生成::appear()
Creator<double> oops; //错误::appear()第2次被生成。

最后一点就是:对于函数的实体处于类定义的内部,所以这些函数时内联函数。因此,在两个不同的翻译单元中可以生成相同的函数。

2. 友元模板

我们通常声明的友元只是:函数模板的实例或者类模板实例,我们指定的友元也只是特定的实体。然而,我们有时候需要让模板的所有实例都成为友元,这就需要声明友元模板。例如:

1
2
3
4
5
6
7
8
9
10
11
class Manager{
template <typename T>
friend class Task;
template <typename T>
friend void Schedule<T>::dispatch(Task<T>*);
template <typename T>
friend int ticket(){
return ++Manager::conter;
}
static int counter;
};

和普通友元的声明一样,只有在友元模板声明的是一个非受限的函数名称,并且后面没有尖括号的情况下,该友元模板声明才能成为定义。

友元模板声明的只是基本模板和基本模板的成员。当进行这些声明之后,与该基本模板相对应的模板局部特换和显式特化都会被自动看成友元。