C++ template —— 深入模板基础(二)

上一篇C++ template —— 模板基础(一)讲解了有关C++模板的大多数概念,日常C++程序设计中所遇到的很多问题,都可以从这部分教程得到解答。本篇中我们深入语言特性。
------------------------------------------------------------------------------------------------------------
第8章 深入模板基础
8.1 参数化声明
函数模板和类模板:

// 类模板
template <typename T>
class List // (1)作为名字空间作用域的类模板
{
public:
template <typename T2>
List(List<T2> const&); // (2)成员函数模板(构造函数)
}; template <typename T>
template <typename T2>
List<T>::List(List<T2> const& b){} // (3)位于类外部的成员函数模板定义
// (4)位于外部名字空间作用域的函数模板
template <typename T>
int length(List<T> const&); // 联合(Union)模板,往往被看作类模板的一种
template <typename T>
union AllocChunk
{
T object;
usigned char bytes[sizeof(T)];
}; // 和普通函数声明一样,函数模板声明也可以具有缺省调用实参(但不能具有缺省模板实参,注意两者的不同)
void fill(Array<T>*, T const& = T()); // 对于基本类型T()为0,这同时也说明了缺省调用实参可以依赖于模板参数

显然,当fill()函数被调用时,如果提供了第2个函数调用参数的话,就不会实例化这个缺省实参。这同时说明了:即使不能基于特定类型T来实例化缺省调用实参,也可能不会出现错误。例如:

class Value
{
public:
Value(int); // 提供了构造函数,不会再隐式提供缺省无参构造函数,所以,也就不存在缺省构造函数
// 如果需要无参构造函数,需要显示声明
}; void init(Array<Value>* array)
{
Value zero(); fill(array, zero); // 正确,提供了第2个调用实参,不会使用 =T()
fill(array); // 错误:使用了=T(),但当T=Value时缺省构造函数无效(因为显示定义了[只有一个参数的构造函数],所以不会产生缺省的无参构造函数)
}

除了两种基本类型的模板之外,还可以使用相似的符号来参数化其他的3种声明。这3种声明分别都有与之对应的类模板成员的定义:

  • (1)类模板的成员函数的定义;
  • (2)类模板的嵌套类成员的定义;
  • (3)类模板的静态数据成员的定义;

如下:

template <int I>   // int I 非类型的模板参数,I接收的不是一个类型,而是一个值(非类型)
class CupBoard
{
void open();
class Shelf;
static double total_weight;
...
}; template <int I>
void CupBoard<I>::open(){ ... } template <int I>
class CupBoard<I>::Shelf { ... }; template <int I>
double CupBoard<I>::total_weight = 0.0;

8.1.1 虚成员函数

  • 成员函数模板不能被声明为虚函数。这是一种需要强制执行的限制,因为虚函数调用机制的普遍实现都使用了一个大小固定的表,每个虚函数都对应表的一个入口。然而,成员函数模板的实例化个数,要等到整个程序都翻译完毕才能确定,这就和表的大小(是固定的)发生了冲突。因此,如果(将来)要支持虚成员函数模板,将需要一种全新的C++编译器和链接器机制。
  • 相反,类模板的普通成员函数可以是虚函数,因为当类被实例化之后,它们的个数是固定的:
template <typename T>
class Dynamic
{
public:
virtual ~Dynamic(); // ok:类模板的普通成员函数,每个Dynamic只对应一个析构函数
template <typename T2>
virtual void copy(T2 const&); // 错误,在确定Dynamic<T>实例的时候,也无法知道copy()的个数
};

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

int C;
class C; // 正确:类名称和非类名称位于不同的名字空间 int X;
template <typename T>
class X; // 错误:和变量X冲突 struct S;
template <typename T>
class S; // 错误:和struct S 冲突

2. 模板名字是具有链接的,当它们不能具有C链接。如下:

extern "C++" template <typename T>
void normal(); // 这是缺省情况,上面的链接规范可以不写 extern "C" template <typename T>
void invalid(); // 错误的:模板不能具有C链接 extern "Xroma" template <typename T>
void xroma_link(); // 非标准的,当某些编译器将来可能支持写Xroma语言的链接兼容性

3. 模板通常具有外部链接(参考博文 存储持续性、作用域和链接性)。唯一的例外就是前面有static修饰符的名字空间作用域下的函数模板:

// external()函数模板 作为一个声明,引用自 位于其他文件的、具有相同名称的实体;
// 即引用位于其他文件的external()函数模板,也称前置声明
template <typename T>
void external();
// internal函数模板(内部链接性) 与其他文件中具有相同名称的模板没有关系
template <typename T>
static void internal();

因此我们知道(由于外部链接):不能在函数内部声明模板[?TODO]。

8.1.3 基本模板

如果模板声明的是一个普通声明,我们就称它声明的是一个基本模板。

  • 基本模板声明是指:没有在模板名称后面添加一对尖括号(和里面实参)的声明,如下:
template <typename T> class Box;
template <typename T> void translate(T*);
  • 显然,当声明局部特化的时候,声明的就是非基本模板:template <typename T> class Point<T, int> {};
  • 另外,函数模板必须是基本模板;

8.2 模板参数

现今存在3种模板参数:

  • (1)类型参数(它们是使用最多的);
  • (2)非类型参数 ;
  • (3)模板的模板参数

注意:模板中,在同一对尖括号内部,位于后面的模板参数声明可以引用前面的模板参数的名称(但前面的不能引用后面的)。

8.2.1 类型参数
类型参数是通过关键字typename或者class引入的:它们两者几乎是等同的。
注意,在模板声明内部,类型参数的作用类似于typedef(类型定义)名称。例如,如果T是一个模板参数,就不能使用诸如class T 等形式的修饰名称,即使T是一个要被class类型替换的参数也不可以:

template <typename Allocator>
class List
{
class Allocator* allocator; // 错误
friend class Allocator; // Error
};

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

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

所有其他的类型现今都不允许作为非类型参数使用。
2. 函数和数值类型也可以被指定为非模板参数,但要把它们先隐式地转换为指针类型,这种转型也称为decay:

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

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

template <int const length> class Buffer;      // 这里的const是没用的,被忽略了

4. 最后,非类型模板参数只能是右值:它们不能被取址,也不能被赋值。

8.2.3 模板的模板参数

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

template <template<typename X> class C>
void f(C<int>* p);

模板的模板参数的参数可以具有缺省模板实参。如下:

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

注意:对于模板的模板参数而言,它的模板参数名称只能被自身其他参数的声明使用。如下:

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

注:由于通常模板的模板参数的参数名称并不会在后面被用到,因此该参数也经常被省略不写,即没有命名。

8.2.4 缺省模板实参

  • 1. 现今,只有类模板声明才能具有缺省模板实参(函数模板不能具有缺省模板实参,但可以有缺省调用实参);
  • 2. 与缺省的函数调用参数的约束一样:对于任一个模板参数(比如T3),只有在T3之后的模板参数(比如T4、T5)都提供了缺省实参的前提下,才能具有缺省模板实参;
  • 3. 后面的缺省值通常是在同个模板声明中提供的,但也可以在前面的模板声明中提供;如下:
template <typename T1, typename T2, typename T3, typename T4 = char, typename T5 = char>
class Quintuple;
//模板特化,其中 (T4,T5)缺省值由前面模板声明提供
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还没有缺省实参
  • 4. 缺省模板实参是从参数列后面向前检查的,函数模板参数演绎是从参数列前面向后演绎的
  • 5. 缺省实参不能重复声明:
template <typename T = void>
class Value; template <typename T = void>
class Value; // 错误:重复出现的缺省实参

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

  • (1)显式模板实参:紧跟在模板名称后面,在一对尖括号内部的显式模板实参值。所组成的整个实体称为template-id。
  • (2)注入式(injecter)类名称:对于具有模板参数P1、P2……的类模板X,在它的作用域中,模板名称(即X)等同于template-id:X<P1, P2, ……>【TODO】。
  • (3)缺省模板实参:如果提供缺省模板实参的话,在类模板的实例中就可以省略显式模板实参。然而,即使所有的模板参数都具有缺省值,一对尖括号还是不能省略的(即使尖括号内部为空,也要保留尖括号)。
  • (4)实参演绎:对于不是显式指定的函数模板实参,可以在函数的调用语句中,根据函数调用实参的类型来演绎出函数模板实参。

8.3.1 函数模板实参

  • 显式指定或者实参演绎;
  • 注意,对于某些模板实参永远也得不到演绎的机会(比如函数返回值类型);
  • 于是我们最好把这些实参所对应的参数放在模板参数列表的开始处(注意是开始处,和缺省模板参数的放置位置顺序相反:为了显式指定模板参数T,需要把T放到参数列表最前;为了提供T的缺省模板实参,需要确保参数列表中位于T后面的模板参数也都提供了缺省实参),从而可以显式指定这些参数,而其他参数仍可以进行实参演绎。如下:
template <typename RT, typename T>
inline RT func(T const &){ ... } template <typename T, typename RT>
inline RT func1(T const &){ ... } int main()
{
double value = func<double>(-); // double 显式指定RT类型,-1实参演绎T类型
double value = func1<int, double>(-); // 要显式指定RT的类型,必须同时显式指定它前面的类型T
}

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

template <typename Func, typename T>
void apply(Func func_ptr, T x)
{
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>, ); // 正确
apply(&multi<int>, ); // 错误:multi<int> 不唯一
}

注:&multi<int>可以是两种函数类型中的任意一种,产生二义性,不能演绎出Func的类型。

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

template<typename T> RT1 test(typename T::X const*);
template<typename T> TR2 test(...);

表达式test<int>可能会使第1个函数模板毫无意义,因为基本int类型根本就没有成员类型X.
显然,“替换失败并非错误(substitution-failure-is-not-an-error, SFINAE)”原则是令函数模板可以重载的重要因素。
SFINAE原则保护的只是:允许试图创建无效的类型,但并不允许试图计算无效的表达式。如:

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

8.3.2 类型实参
模板的类型实参是一些用来指定模板类型参数的值。我们平时使用的大多数类型都可以被用作模板的类型实参。但有两种情况例外:
(1)局部类和局部枚举(换句话说,指在函数定义内部声明的类型)不能作为模板的类型实参;
(2)未命名的class类型或者未命名的枚举类型不能作为模板的类型实参(然而,通过typedef声明给出的未命名类和枚举是可以作为模板类型实参的)。

template <typename T> class List
{
...
}; typedef struct
{
double x, y, z;
}Point; typedef enum { red, green, blue } *ColorPtr; int main()
{
struct Association
{
int* p;
int* q;
};
List<Association*> error1; // 错误:模板实参中使用了局部类型
// 错误:模板实参中使用了未命名的类型,因为typedef定义的是*ColorPtr,并非ColorPtr
List<ColorPtr> error2;
List<Point> ok; // 正确:通过使用typedef定义的未命名类型
}

8.3.3 非类型实参

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

(1)某一个具有正确类型的非类型模板参数;

(2)一个编译期整型常值(或枚举值);这只有在参数类型和值的类型能够进行匹配,或者值的类型可以隐式地转换为参数类型的前提下,才是合法的。

(3)前面有单目运算符&(即取址)的外部变量或者函数的名称。对于函数或数组变量,&运算符可以省略。这类模板实参可以匹配指针类型的非类型参数。

(4)对于引用类型的非类型模板参数,前面没有&运算符的外部变量和外部函数也是可取的;

(5)一个指向成员的指针常量;换句话说,类似&C::m的表达式,其中C是一个class类型,m是一个非静态成员(成员变量或者函数)。这类实参只能匹配类型为“成员指针”的非类型参数。

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

template <typename T, T nontyep_param>
class C; C<int, >* c1; // 整型 int a;
C<int*, &a>* c2; // 外部变量的地址 void f();
void f(int);
C<void(*)(int), f>* c3; // 函数名称:在这个例子中,重载解析会选择f(int),f前面的&隐式省略了 class X
{
public:
int n;
static bool b;
}; C<bool&, X::b>* c4; // 静态成员是可取的变量(和函数)名称 C<int X::*, &X::n>* c5; // 指向成员的指针常量 template<typename T>
void templ_func(); C<void(), &templ_func<double> >* c6; // 函数模板实例同时也是函数

注意:模板实参的一个普遍约束是:在程序创建的时候,编译器或者链接器要能够确定实参的值。如果实参的值要等到程序运行时才能够确定(譬如,局部变量的地址),就不符合“模板是在程序创建的时候进行实例化”的概念了。(模板实参是一个在编译期可以确定的值,这样才符合“模板是在程序创建的时候进行实例化”的概念。)
另一方面,有些常值不能作为有效的非类型实参:空指针常量、浮点型值、字符串。

template <typename T, T nontyep_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[];
C<int*, &a[]>* err3; // 错误:单一数组元素的地址并不是可取的

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

template <typename T1,
typename T2,
template<typename T,
typename = std::allocator<T> > class Container>
// Container现在就能够接受一个标准容器模板了
class Relation
{
public:
...
private:
Container<T1> dom1;
Container<T2> dom2;
};

2. 从语法上讲,只有关键字class才能被用来声明模板的模板参数;但是这并不意味只有用关键字class声明的类模板才能作为它的替换实参。实际上,“struct模板”、“union模板”都可以作为模板的模板参数的有效实参。这和我们前面所提到的事实很相似:对于用关键字class声明的模板类型参数,我们可以用(满足约束的)任何类型作为它的替换实参。

8.3.5 实参的等价性

当每个对应实参值都相等时,我们就称这两组模板实参是相等的。对于类型实参,typedef名称并不会对等价性产生影响;就是说,最后比较的还是typedef原本的类型。对于非类型的整型实参,进行比较的是实参的值;至于这些值是如何表达的,也不会产生影响。
另外,从函数模板产生(即实例化出来)的函数一定不会等于普通函数,即便这两个函数具有相同的类型和名称。这样,针对类成员,我们可以引申出两点结论:
(1)从成员函数模板产生的函数永远也不会改写一个虚函数(进一步说明成员函数模板不能是一个虚函数)。(虚函数表有固定大小)。

(2)从构造函数模板产生的构造函数一定不会是缺省的拷贝构造函数(类似,从赋值运算符模板产生的赋值运算符也一定不会是一个拷贝赋值运算符。但是,后面这种情况通常不会出现问题,因为与拷贝构造函数不同的是:赋值运算符永远也不会被隐式调用)。

8.4 友元

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

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

template <typename T>
class Node; template <typename T>
class Tree
{
friend class Node<T>;
...
};

显然,如果要把类模板的实例声明为其他类(或者类模板)的友元,该类模板在声明的地方必须是可见的(这里的意思是要求类模板有前置声明或者声明出能看到的定义,因为类模板从T实例化出来的实体会根据类模板的定义来考量类型T是否提供了所需要的操作)。然而对于一个普通类,就没有这个要求:

template <typename T>
class Tree
{
friend class Factory; // 正确:即使这里是Factory的首次声明
friend class Node<T>; // 如果Node在此是不见见的,这条语句就是错误的
};

8.4.1 友元函数
通过确认紧接在友元函数名称后面的是一对尖括号,我们可以把函数模板的实例声明为友元。

template <typename T1, typename T2>
void combine(T1, T2); class Mixer
{
friend void combine<>(int&, int&); // 正确:T1 = int&, T2 = int&
friend void combine<int, int>(int, int); // 正确:T1 = int, T2 = int
friend void combine<char>(char, int); // 正确:T1 = char, T2 = int
friend void combine<char>(char&, int&); // 错误:不能匹配上面的combine()模板
friend void combine<>(long, long){ ... }; // 错误:这里的友元声明不允许出现定义
};

另外应该知道:我们不能再友元声明中定义一个模板实例(我们最多只能定义一个特化);因此,命名一个实例的友元声明是不能作为定义的。
如果名称后面没有紧跟一对尖括号,那么只有在下面两种情况下是合法的:
(1)如果名称不是受限的(就是说,没有包含一个形如双冒号的域运算符),那么该名称一定不是(也不能)引用一个模板实例。如果在友元声明的地方,还看不到所匹配的非模板函数(即普通函数),那么这个友元声明就是函数的首次声明。于是,该声明可以是定义。(非受限、首次声明——可以定义)
(2)如果名称是受限的(就是说前面有双冒号::),那么该名称必须引用一个在此之前声明的函数或者函数模板。在匹配的过程中,匹配的函数要优先于匹配的函数模板。然而,这样的友元声明不能是定义。(受限——不能定义)

void multiply(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() { } // 错误:受限的友元不能是一个定义
};

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

template <typename T>
class Node
{
Node<T>* allocate();
....
}; template <typename T>
class List
{
friend Node<T>* Node<T>::allocate();
};

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

template <typename T>
class Creator
{
friend void appear() { ... } //一个新函数::appear(),但要等到Creator被实例化之后才存在
}; Creator<void> miracle; // 这时才生成::appear()
Creator<double> oops; // 错误:::appear()第2次被生成

在这个例子中,两个不同的实例化过程生成了两个完全相同的定义(即appear函数),这违反了ODR原则。

因此,我们必须确定:在模板内部定义的友元函数的类型定义中,必须包含类模板的模板参数(除非我们希望在一个特定的文件中禁止多于一个的实例被创建,但这种用法很少)。修改上面代码如下:

template <typename T>
class Creator
{
friend void feed(Creator<T>*) { ... } //每个T都生成一个不同的 ::feed()函数
}; Creator<void> miracle; // 生成 ::feed(Creator<void>*)
Creator<double> oops; // 生成 ::feed(Creator<double>*)

在这个例子中,每个Creator的实例都生成了一个不同的feed()函数。另外我们应该知道:尽管这些函数是作为模板的一部分被生成的,但函数本身仍然是普通函数,而不是模板的实例。最后一点就是:由于函数的实体处于类定义的内部,所以这些函数是内联函数。因此,在两个不同的翻译单元中可以生成相同的函数。

8.4.2 友元模板

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

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::counter;
};
static int counter;
};

和普通友元的声明一样,只有在友元模板声明的是一个非受限的函数名称,并且后面没有紧跟尖括号的情况下,该友元模板声明才能成为定义。
友元模板声明的只是基本模板和基本模板的成员。当进行这些声明之后,与该基本模板相对于的模板局部特化和显式特化都会被自动地看成友元。

template <typename T> class Box;
template <typename T> void translate(T*);
上一篇:JSP(include指令)页面


下一篇:Git Commit Template 提交模板