What are you, Anyway?
作者:Stephen C. Dewhurst 译者:陶章志
原文出处:http://www.cuj.com/documents/s=8464/cuj0308dewhurst/
在经过艰难的讨论template metaprogramming很长时间后,返回到我们学习的开始。
在这一部分,我们来了解模板编程的更为模糊的语法问题:在编译器没有充分的信息的情况下,怎样引导编译器进行分析。在这里,我们将讨论标准容器中用来消除歧义的“rebind”机制。同时,我们也将对一些潜在的模板编程技术进行热烈的讨论。
甚至经验丰富的C++程序员,也常常被模板的复杂的语法所困扰。在所以模板语法中,我们首先要了解:消除编译器分析的歧义,是最基本的语法困惑。
Types of Names, Names of Types
让我们看看一个没有实现标准容器的模板例子,这个例子很简单。
template <typename T>
class PtrList {
public:
//...
typedef T *ElemT;
void insert( ElemT );
private:
//...
};
常常模板类嵌入Type names信息,这样,我们就可以通过正确的nested name 获得实例化的模板信息。
typedef PtrList<State> StateList;
//...
StateList::ElemT currentState = 0;
嵌入类型的ElenT允许我们可以,很容易的访问PtrList模板的所承认的元素类型。
即使我们用State类型初始化PtrList,元素类型还将是State*。在其他一些情况下,PtrList 可以用指针元素实现。一个比较成熟的PtrList的实现,应该是可以随着初始化的元素类型而变化的。使用nested type,可以帮助我们封装PtrList,以免用户了解内部的实现。
下面还有一个例子:
template <typename Etype>
class SCollection {
public:
//...
typedef Etype ElemT;
void insert( const Etype & );
private:
//...
};
SCollection的实现跟PtrList一样,遵守标准命名的条款。遵守这些条款是有用的,这样我们就可以写出很多优雅的算法来使用这些容器(译注:像标准模板库一样)。例如:可以写一个如下的算法:用适当的元素类型来填充这个容器数组。
template <class Cont>
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<State> StateList;
//...
StateList::ElemT aState = 0;
PtrList<State>::ElemT anotherState = 0;
对于编译器来说,一个实例化的模板类跟一个普通的类一样。在存储PtrList<State>的nested name 和在MyContainer中是一样的,没有什么差别。在其他情况下,编译器也是这样检查上下文来看ElemT是不是type name。然而,当我们进入template的上下文后,事情就变得复杂了。因为在这,没有充分的准确信息。考虑下面的程序片断:
template <typename T>
void aFuncTemplate( T &arg ) {
...T::ElemT...
当编译器遇到T::ElemT,它不知道这是什么。从模板的申明中,编译器知道,T是一个类型名。它通过::运算符也能猜测出T是一个类型名。但是,这就是所有编译器知道的。因为,这里没有关于T的更多的信息。例如:我们能够用PtrList来调用一个模板函数,在这里,T::ElemT将是一个Type name。
PtrList<State> states;
//...
aFuncTemplate( states ); // T::ElemT is PtrList<State>::ElemT
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 <typename T>
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<State>::ElemT is a type name
现在,我们可以重写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<State>::ElemT elem; // OK
typename PtrList<State>::ElemT elem; // error!
在模板的上下文中,这是很常见的错误。考虑一个在模板,在它内部实现,在编译时刻,从两个类型中选出一个,例如:
Select<cond,int,int *>::R r1; // OK
typename Select<cond,int,int *>::R r2; // error!
//...
}
由于编译器可以获得所有模板参数的信息,因此,甚至不需要在Select前写typename。如果,用模板重写f(),我们就可以使用typename。
template <typename T>
void f() {
Select<cond,int,int *>::R r1; // #1: OK, typename not required
typename Select<cond,int,int *>::R r2; // #2: superfluous
Select<cond,T,T *>::R r3; // #3: error! need typename
typename Select<cond,T,T *>::R r4; // #4: OK
//...
}
在情况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 <typename T>
T &get() {
static T member;
return member;
}
};
注意:这个模板函数可以在编译时,按需要初始化,很遗憾的,它不能是虚拟函数。这个版本的Monostate为分享静态成员,实现了"lazy creation" 。
Monostate m;
m.get<int>() = 12; // create an int member
Monostate m2;
cout << m2.get<int>(); // access previously-created member
m2.get<std::string>() = "Hej!" // create a string member
注意: 不像传统的Singleton的"lazy creation"那样,这个"lazy creation"作用于编译时刻,而不是运行时刻。
Indexed Expanding Monostate
这个办法其实还很不理想,至少如果用户想有多个分享的特殊类型的成员,那么又该怎么办?一种改善的办法是给模板成员函数添加一个参数“index”。
class IndexedMonostate {
public:
template <typename T, int i>
T &get();
};
template <typename T, int i>
T &IndexedMonostate::get() {
static T member;
return member;
}
现在,我们可以拥有多个特殊类型的成员了,但是这个接口还可以更加完善。
IndexedMonostate im1, im2;
im2.get<int,1066>() = 12;
im2.get<double,42>() = im2.get<int,1066>()+1;
Named Expanding Monostate
我们所需要的是记录用户的使用Monostate成员的类型。这个类型也是为模板函数的包装的类型和static成员的实际类型。
template <typename T, int n>
struct Name {
typedef T Type;
};
这个Name类看上去很简单,但是它已经足够满足要求。
typedef Name<int,86> grossAmount;
typedef Name<double,007> percentage;
现在我们可以可读类型,而且还可以把成员类型和index绑定在一起。注意:这index对应的实际数值不是实质性的,只要[type,index] 是唯一的。一个命名的Monostate假定成员的类型能够从它的初始化类型解压。
class NamedMonostate {
public:
template <class N>
typename N::Type &get() {
static typename N::Type member;
return member;
}
};
这个提高用户接口的技术是没有牺牲原来技术的简单性和方便性(注意:typename是告诉嵌入的N::Type是一个type name)。
可以这样使用:
NamedMonostate nm1, nm2;
nm1.get<grossAmount>() = 12;
nm2.get<percentage>() = nm1.get<grossAmount>() + 12.2;
cout << nm1.get<grossAmount>() * nm2.get<percentage>() << endl;
最后,我们可以修改接口来使用Monostate。
class GSNamedMonostate {
public:
template <typename N>
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<typename N::Type &>(get()) = val;
}
template <typename N>
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<int,86> grossAmount;
typedef Name<double,007> percentage;
GSNamedMonostate nm1, nm2;
nm1.set<grossAmount>( 12 );
nm2.set<percentage>( nm1.get<grossAmount>() + 12.2 );
cout << nm1.get<grossAmount>() * nm2.get<percentage>() << endl;
在上面的代码中,编译器在检查模板get不会碰到任何困难。 其中,nm1和nm2是GSNamedMonostate的类型名,编译器可以在类里面查询get和set的类型。
然而,考虑写这样一个优雅的函数:它能够用来移置扩展的Monostate object。
template <typename M>
void populate() {
M m;
m.get<grossAmount>(); // syntax error!
M *mp = &m;
mp->get<percentage>(); // syntax error!
}
又一次,问题出在编译器不知道M足够的信息,除了,知道它是type name外。特别是,如果没有足够的get<>信息的话,编译器会认为它不是type,不是模板名。因此,m.get<grossAmount>()的中括号被解释为大于号,和小于号,而不是模板参数列表。
这种情况下,解决办法是要告诉编译器<>是模板参数列表,而不是其他的操作名。
template <typename M>
void populate() {
M m;
m.template get<grossAmount>(); // OK
M *mp = &m;
mp->template get<percentage>(); // OK
}
是不是不可思议啊,就像分析使用typename一样,这种template特殊的用法,仅在必要的情况下,才能使用。
Hints For Rebinding Allocators
我们也碰到嵌入模板类的同样的分析问题,在STL allocator的实现,就是这样的经典例子。
template <class T>
class AnAlloc {
public:
//...
template <class Other>
class rebind {
public:
typedef AnAlloc<Other> other;
};
//...
};
这个模板类AnAlloc中就有嵌入的name,而这个name本身就是一个模板类。这是使用STL的框架来创建allocators,就像allocators为一个容器用不同的数据类型初始化一样。例如:
typedef AnAlloc<int> AI; // original allocator allocates ints
typedef AI::rebind<double>::other AD; // new one allocates doubles
typedef AnAlloc<double> AD; // legal! this is the same type
也许,这样看起来是有些多余。但是使用rebind机制可以允许我们用现存的allocator为不同的数据类型工作,而且不需要知道当前的allocator类型和要allocate数据类型。
typedef SomeAlloc::rebind<ListNode>::other NewAlloc;
如果SomeAlloc要为STL的allocators提供方便的话,它要有嵌入的rebind 模板类。本质上说:“我们不要知道allocator的类型,也不要知道分配类型,但是,我想要一个像allocates ListNodes一样的allocator”。
在模板中常常忽视这种工作,直到template 初始化后,变量的类型和值才能确定。考虑STL各种编译List容器的实现,我们的模板列表有两个模板参数,一个元素类型(T)和allocator type(A)。(像标准容器,我们list提供缺省的allocator )。
template < typename T, typename A = std::allocator<T> >
class OurList {
struct Node {
//...
};
typedef A::rebind<Node>::other NodeAlloc; // error!
};
作为典型基于lists基础的容器,我们的list实际上不分配和操作元素Ts。而是,分配和操作T类型的容器。这种情况,就是我们前面所讲述的。我们有allocator,它知道怎样分配T类型的对象,但是,我们想分配OurList<T,A>::Node。然而,当我们尝试这么rebind的时候,我们会出现语法错误。 这个问题再一次是因为编译器没有A类型足够的信息。因此,编译器认为嵌入的rebind name不是模板name,同时,<>被解释为大于,小于操作。但是,这只是我们问题的开始。就算编译器能够知道rebind 是template name,它也会认为不是type name。因此,必须这么写typedef。
typedef typename A::template rebind<Node>::other NodeAlloc;
关键字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 (<www.semantics.org>) 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 (<www.gotw.ca/cpp_seminar>