2008年(909)
分类:
2008-05-06 21:50:32
原文出处:http://www.cuj.com/documents/s=8464/cuj0308dewhurst/
在经过艰难的讨论template metaprogramming很长时间后,返回到我们学习的开始。
在这一部分,我们来了解模板编程的更为模糊的语法问题:在编译器没有充分的信息的情况下,怎样引导编译器进行分析。在这里,我们将讨论标准容器中用来消除歧义的“rebind”机制。同时,我们也将对一些潜在的模板编程技术进行热烈的讨论。
甚至经验丰富的C 程序员,也常常被模板的复杂的语法所困扰。在所以模板语法中,我们首先要了解:消除编译器分析的歧义,是最基本的语法困惑。
Types of Names, Names of Types
让我们看看一个没有实现标准容器的模板例子,这个例子很简单。
template
class PtrList {
public:
//...
typedef T *ElemT;
void insert( ElemT );
private:
//...
};
常常模板类嵌入Type names信息,这样,我们就可以通过正确的nested name 获得实例化的模板信息。
typedef PtrList
//...
StateList::ElemT currentState = 0;
嵌入类型的ElenT允许我们可以,很容易的访问PtrList模板的所承认的元素类型。
即使我们用State类型初始化PtrList,元素类型还将是State*。在其他一些情况下,PtrList 可以用指针元素实现。一个比较成熟的PtrList的实现,应该是可以随着初始化的元素类型而变化的。使用nested
type,可以帮助我们封装PtrList,以免用户了解内部的实现。
下面还有一个例子:
template
class SCollection {
public:
//...
typedef Etype ElemT;
void insert( const Etype & );
private:
//...
};
SCollection的实现跟PtrList一样,遵守标准命名的条款。遵守这些条款是有用的,这样我们就可以写出很多优雅的算法来使用这些容器(译注:像标准模板库一样)。例如:可以写一个如下的算法:用适当的元素类型来填充这个容器数组。
template
void fill( Cont &c, Cont::ElemT a[], int len ) { // error!
for( int i = 0; i < len; i )
c.insert( a[i] );
}
蹩脚的编译器
很遗憾的是,在这里我们有一个语法错误。编译器不能识别Cont::ElemT这个type name。问题是在fill()的上下文中,没有足够的信息让编译器知道ElemT是一个type
name。在标准中规定,在这种情况下,认为nested name 不是type name。 现在刚刚开始,如果你没有理解,不要紧。我们来看看在不同的上下文中,编译器所获得的信息。首先,让我们来看看在没有模板的class的情况:
class MyContainer {
public:
typedef State ElemT;
//...
};
//...
MyContainer::ElemT *anElemPtr = 0;
由于编译器可以检测到MyContainer class的上下文确定有个ElemT的成员类型,从而可以确认MyContainer::ElemT确实是一个type name。在实例化的模板类中,其实,也跟这种情况一样简单。
typedef PtrList
//...
StateList::ElemT aState = 0;
PtrList
对于编译器来说,一个实例化的模板类跟一个普通的类一样。在存储PtrList
template
void aFuncTemplate( T &arg ) {
...T::ElemT...
当编译器遇到T::ElemT,它不知道这是什么。从模板的申明中,编译器知道,T是一个类型名。它通过::运算符也能猜测出T是一个类型名。但是,这就是所有编译器知道的。因为,这里没有关于T的更多的信息。例如:我们能够用PtrList来调用一个模板函数,在这里,T::ElemT将是一个Type name。
PtrList
//...
aFuncTemplate( states ); // T::ElemT is PtrList
But suppose we were to instantiate aFuncTemplate with a different type?
struct X {
double ElemT;
//...
};
X anX;
//...
aFuncTemplate( anX ); // T::ElemT is X::ElemT
在这个例子中,T::ElemT是数据类型,不是type name。编译器将怎么办呢?在标准中规定,在这种情况下,编译器将认为nested name 不是type name。在将在上述fill()模板函数中导致一个语法错误。
Clue In the Compiler
为了处理这种情况,我们必须清晰的提示编译器:
这个nested name 是type name。如下:
template
void aFuncTemplate( T &arg ) {
...typename T::ElemT...
在这里,我们使用关键字typename 来告诉编译器后面跟着的name,是type name。这样使得编译器可以正确的分析template。注意:我们告诉编译器:ElemT而不是T,是Type name。当然,编译器也能够知道T也是type name。同样,如果我们这样写:
typename A::B::C::D::E
这样,我们就相当于告诉编译器,E是type name。当然,如果模板函数传入的类型不满足template分解要求的话,会导致一个编译时刻的编译错误。
struct Z {
// no member named ElemT...
};
Z aZ;
//...
aFuncTemplate( aZ ); // error! no member Z::ElemT
aFuncTemplate( anX ); // error! X::ElemT is not a type name
aFuncTemplate( states ); // OK. PtrList
现在,我们可以重写fill()模板函数,
void fill( Cont &c, typename Cont::ElemT a[], int len ) { // OK
for( int i = 0; i < len; i )
c.insert( a[i] );
}
Gotcha: Failure to Employ typename with Permissive Compilers
注意:
使用typename 要求 嵌入 type name,如果编译器不能得到足够的信息的话,在模板的外部使用typename是非法的。
PtrList
typename PtrList
在模板的上下文中,这是很常见的错误。考虑一个在模板,在它内部实现,在编译时刻,从两个类型中选出一个,例如:
Select
typename Select
//...
}
由于编译器可以获得所有模板参数的信息,因此,甚至不需要在Select前写typename。如果,用模板重写f(),我们就可以使用typename。
template
void f() {
Select
typename Select
Select
typename Select
//...
}
在情况2中,typename,可以不写,这样是可以的。
最有问题的是情况3,很多编译器都能察觉这个错误,将把这个嵌入的R解释为type name(的确它是一个type name,但是没有希望它解释为type name)以后,如果,这段代码出现在标准编译器上,那么会被查出错误的。因为这个原因,当你用C 模板编程,如果你必须使用非标准编译器的,你最好使用高级标准编译器,来检查你的代码。
Intermezzo: Expanding Monostate Protopattern
在模板问题上,我们先停顿一下,让我们看看搜索技术。 当我们想避免Monostate常常是Singleton的很好替代技术。当为了避免全局变量带来的麻烦时,Monostate是Singleton的很好替代品。
class Monostate {
public:
int getNum() const { return num_; }
void setNum( int num ) { num_ = num; }
const std::string &getName() const { return name_; }
private:
static int num_;
static std::string name_;
};
就像Singleton一样,Monostate 提供对象的简单copy,不像典型的Singleton,这种分享机制不是由构造函数实现的。而是通过存储静态成员。注意:Monostate不同于传统的使用静态成员机制,传统的办法是通过静态成员函数来存储静态成员变量。
Monostate提供非静态成员函数来存储静态成员变量。(译注:好方法,我们来看作者怎么实现的)
Monostate m1;
Monostate m2;
//...
m1.setNum( 12 );
cout << m2.getNum() << endl; // shift 12
每一个不同类型的Monostate分享相同的状态。Monostate没有使用任何特殊的语法,不像Singleton的实现。
Singleton::instance().setNum( 12 );
cout << Singleton::instance().getNum() << endl;
Expanding Monostate
如果我们想在Monostate中添加新的静态成员,那么该怎么实现?理想的情况是不添加操作不需要改变源代码,甚至不要重编译不相关的代码。让我们来看看怎样使用template来实现这个任务的。
class Monostate {
public:
template
T &get() {
static T member;
return member;
}
};
注意:这个模板函数可以在编译时,按需要初始化,很遗憾的,它不能是虚拟函数。这个版本的Monostate为分享静态成员,实现了"lazy creation" 。
Monostate m;
m.get
Monostate m2;
cout << m2.get
m2.get
注意: 不像传统的Singleton的"lazy creation"那样,这个"lazy creation"作用于编译时刻,而不是运行时刻。
Indexed Expanding Monostate
这个办法其实还很不理想,至少如果用户想有多个分享的特殊类型的成员,那么又该怎么办?一种改善的办法是给模板成员函数添加一个参数“index”。
class IndexedMonostate {
public:
template
T &get();
};
template
T &IndexedMonostate::get() {
static T member;
return member;
}
现在,我们可以拥有多个特殊类型的成员了,但是这个接口还可以更加完善。
IndexedMonostate im1, im2;
im2.get
im2.get
Named Expanding Monostate
我们所需要的是记录用户的使用Monostate成员的类型。这个类型也是为模板函数的包装的类型和static成员的实际类型。
template
struct Name {
typedef T Type;
};
这个Name类看上去很简单,但是它已经足够满足要求。
typedef Name
typedef Name
现在我们可以可读类型,而且还可以把成员类型和index绑定在一起。注意:这index对应的实际数值不是实质性的,只要[type,index] 是唯一的。一个命名的Monostate假定成员的类型能够从它的初始化类型解压。
class NamedMonostate {
public:
template
typename N::Type &get() {
static typename N::Type member;
return member;
}
};
这个提高用户接口的技术是没有牺牲原来技术的简单性和方便性(注意:typename是告诉嵌入的N::Type是一个type name)。
可以这样使用:
NamedMonostate nm1, nm2;
nm1.get
nm2.get
cout << nm1.get
最后,我们可以修改接口来使用Monostate。
class GSNamedMonostate {
public:
template
void set( const typename N::Type &val ) {
// This const_cast is actually safe,
// since we are always actually getting
// a non-const object. (Unless N::Type is
// const, then you get a compile error here.)
const_cast
}
template
const typename N::Type &get() const {
static typename N::Type member;
return member;
}
};
这是原型模式(Protopattern)吗?
其实,像我们刚刚开始提到的一样,这是搜索技术。同样,我们没有权利调用这样的模式。一个设计模式是包装了成功的实际成果的。这个"protopattern"通常应用在上下文中可以察觉的技术,因此,不能被应用于更加广泛的“pattern”软件中。由于我们不能指出它的成功之地方,所以,我们只能尽量扩展monostate这个模式。
Template Names in Templates
让我们回到分析模板的编译器问题上来吧。编译器分析的难题,不仅只有嵌入type names,而且,我们还常常见到嵌入 template names 类似的问题。调用一个类,或类模板必须有一个这样的成员。这个成员是一个类,或模板函数。
例如:一个使用模板成员函数的扩展Monostate可以按需要这样初始化:
typedef Name
typedef Name
GSNamedMonostate nm1, nm2;
nm1.set
nm2.set
cout << nm1.get
在上面的代码中,编译器在检查模板get不会碰到任何困难。 其中,nm1和nm2是GSNamedMonostate的类型名,编译器可以在类里面查询get和set的类型。
然而,考虑写这样一个优雅的函数:它能够用来移置扩展的Monostate object。
template
void populate() {
M m;
m.get
M *mp = &m;
mp->get
}
又一次,问题出在编译器不知道M足够的信息,除了,知道它是type name外。特别是,如果没有足够的get<>信息的话,编译器会认为它不是type,不是模板名。因此,m.get
这种情况下,解决办法是要告诉编译器<>是模板参数列表,而不是其他的操作名。
template
void populate() {
M m;
m.template get
M *mp = &m;
mp->template get
}
是不是不可思议啊,就像分析使用typename一样,这种template特殊的用法,仅在必要的情况下,才能使用。
Hints For Rebinding Allocators
我们也碰到嵌入模板类的同样的分析问题,在STL allocator的实现,就是这样的经典例子。
template
class AnAlloc {
public:
//...
template
class rebind {
public:
typedef AnAlloc
};
//...
};
这个模板类AnAlloc中就有嵌入的name,而这个name本身就是一个模板类。这是使用STL的框架来创建allocators,就像allocators为一个容器用不同的数据类型初始化一样。例如:
typedef AnAlloc
typedef AI::rebind
typedef AnAlloc
也许,这样看起来是有些多余。但是使用rebind机制可以允许我们用现存的allocator为不同的数据类型工作,而且不需要知道当前的allocator类型和要allocate数据类型。
typedef SomeAlloc::rebind
如果SomeAlloc要为STL的allocators提供方便的话,它要有嵌入的rebind 模板类。本质上说:“我们不要知道allocator的类型,也不要知道分配类型,但是,我想要一个像allocates
ListNodes一样的allocator”。
在模板中常常忽视这种工作,直到template 初始化后,变量的类型和值才能确定。考虑STL各种编译List容器的实现,我们的模板列表有两个模板参数,一个元素类型(T)和allocator
type(A)。(像标准容器,我们list提供缺省的allocator )。
template < typename T, typename A = std::allocator
class OurList {
struct Node {
//...
};
typedef A::rebind
};
作为典型基于lists基础的容器,我们的list实际上不分配和操作元素Ts。而是,分配和操作T类型的容器。这种情况,就是我们前面所讲述的。我们有allocator,它知道怎样分配T类型的对象,但是,我们想分配OurList
typedef typename A::template rebind
关键字template告诉编译器这个rebind是模板名,关键字typename告诉编译器整个指向一个type name,很简单吧。
参考资料和注意事项:
[1]这样的接口并不总是一个好的主意。参考 Gotcha #80: Get/Set Interfaces in C Gotchas (Addison-Wesley,
2003).
[2]事实上,你也许可以不这样做,尽管从哲学的角度来说,populate是一个很有意思的模板函数,它是为很多模板在编译时刻初始化服务的。这样,不需要在编译时刻调用函数了(译注:虚拟函数就是运行时刻初始化)然而,如果函数没有调用,它将不被初始化,这种初始化也不将完成。其他可行的方法就是得到函数的地址,而不是调用函数,或者作一个明显的初始化,这样,如果,函数在运行时刻不需要,它也会存在。
[3]如果你不熟悉STL的allocator,你不要担心,在以后的讨论中,不需要对它熟悉。allocator就是一个类而已,只不过,它是用来为STL容器管理内存的。Allocators是模板类的典型的实现。
About the Author
Stephen C. Dewhurst (<) is the president of Semantics Consulting, Inc., located among the cranberry bogs of southeastern Massachusetts. He specializes in C consulting, and training in advanced C programming, STL, and design patterns. Steve is also one of the featured instructors of The C Seminar (<
下载本文示例代码