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> 
|