Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2095687
  • 博文数量: 909
  • 博客积分: 4000
  • 博客等级: 上校
  • 技术积分: 12260
  • 用 户 组: 普通用户
  • 注册时间: 2008-05-06 20:50
文章分类

全部博文(909)

文章存档

2008年(909)

我的朋友

分类:

2008-05-06 21:50:32

一起学习
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
class PtrList {
public:
//...
typedef T *ElemT;
void insert( ElemT );
private:
//...
};

常常模板类嵌入Type names信息,这样,我们就可以通过正确的nested name 获得实例化的模板信息。

typedef PtrList StateList;
//...
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;
//...
StateList::ElemT aState = 0;
PtrList::ElemT anotherState = 0;

对于编译器来说,一个实例化的模板类跟一个普通的类一样。在存储PtrList的nested name 和在MyContainer中是一样的,没有什么差别。在其他情况下,编译器也是这样检查上下文来看ElemT是不是type name。然而,当我们进入template的上下文后,事情就变得复杂了。因为在这,没有充分的准确信息。考虑下面的程序片断:

template
void aFuncTemplate( T &arg ) {
...T::ElemT...

当编译器遇到T::ElemT,它不知道这是什么。从模板的申明中,编译器知道,T是一个类型名。它通过::运算符也能猜测出T是一个类型名。但是,这就是所有编译器知道的。因为,这里没有关于T的更多的信息。例如:我们能够用PtrList来调用一个模板函数,在这里,T::ElemT将是一个Type name。

PtrList states;
//...
aFuncTemplate( states ); // T::ElemT is PtrList::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
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::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::ElemT elem; // OK
typename PtrList::ElemT elem; // error!
在模板的上下文中,这是很常见的错误。考虑一个在模板,在它内部实现,在编译时刻,从两个类型中选出一个,例如:
Select::R r1; // OK
typename Select::R r2; // error!
//...
}

由于编译器可以获得所有模板参数的信息,因此,甚至不需要在Select前写typename。如果,用模板重写f(),我们就可以使用typename。
template
void f() {
Select::R r1; // #1: OK, typename not required
typename Select::R r2; // #2: superfluous
Select::R r3; // #3: error! need typename
typename Select::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
T &get() {
static T member;
return member;
}
};

注意:这个模板函数可以在编译时,按需要初始化,很遗憾的,它不能是虚拟函数。这个版本的Monostate为分享静态成员,实现了"lazy creation" 。

Monostate m;
m.get() = 12; // create an int member
Monostate m2;
cout << m2.get(); // access previously-created member
m2.get() = "Hej!" // create a string member

注意: 不像传统的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() = 12;
im2.get() = im2.get() 1;

Named Expanding Monostate
我们所需要的是记录用户的使用Monostate成员的类型。这个类型也是为模板函数的包装的类型和static成员的实际类型。

template
struct Name {
typedef T Type;
};

这个Name类看上去很简单,但是它已经足够满足要求。

typedef Name grossAmount;
typedef Name percentage;

现在我们可以可读类型,而且还可以把成员类型和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() = 12;
nm2.get() = nm1.get() 12.2;
cout << nm1.get() * nm2.get() << endl;

最后,我们可以修改接口来使用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(get()) = val;
}

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 grossAmount;
typedef Name percentage;
GSNamedMonostate nm1, nm2;
nm1.set( 12 );
nm2.set( nm1.get() 12.2 );
cout << nm1.get() * nm2.get() << endl;

在上面的代码中,编译器在检查模板get不会碰到任何困难。 其中,nm1和nm2是GSNamedMonostate的类型名,编译器可以在类里面查询get和set的类型。

然而,考虑写这样一个优雅的函数:它能够用来移置扩展的Monostate object。

template
void populate() {
M m;
m.get(); // syntax error!
M *mp = &m;
mp->get(); // syntax error!

}
又一次,问题出在编译器不知道M足够的信息,除了,知道它是type name外。特别是,如果没有足够的get<>信息的话,编译器会认为它不是type,不是模板名。因此,m.get()的中括号被解释为大于号,和小于号,而不是模板参数列表。
这种情况下,解决办法是要告诉编译器<>是模板参数列表,而不是其他的操作名。

template
void populate() {
M m;
m.template get(); // OK
M *mp = &m;
mp->template get(); // OK
}

是不是不可思议啊,就像分析使用typename一样,这种template特殊的用法,仅在必要的情况下,才能使用。
Hints For Rebinding Allocators
我们也碰到嵌入模板类的同样的分析问题,在STL allocator的实现,就是这样的经典例子。

template
class AnAlloc {
public:
//...
template
class rebind {
public:
typedef AnAlloc other;
};
//...
};

这个模板类AnAlloc中就有嵌入的name,而这个name本身就是一个模板类。这是使用STL的框架来创建allocators,就像allocators为一个容器用不同的数据类型初始化一样。例如:

typedef AnAlloc AI; // original allocator allocates ints
typedef AI::rebind::other AD; // new one allocates doubles
typedef AnAlloc AD; // legal! this is the same type

也许,这样看起来是有些多余。但是使用rebind机制可以允许我们用现存的allocator为不同的数据类型工作,而且不需要知道当前的allocator类型和要allocate数据类型。

typedef SomeAlloc::rebind::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 >
class OurList {
struct Node {
//...
};
typedef A::rebind::other NodeAlloc; // error!
};

作为典型基于lists基础的容器,我们的list实际上不分配和操作元素Ts。而是,分配和操作T类型的容器。这种情况,就是我们前面所讲述的。我们有allocator,它知道怎样分配T类型的对象,但是,我们想分配OurList::Node。然而,当我们尝试这么rebind的时候,我们会出现语法错误。 这个问题再一次是因为编译器没有A类型足够的信息。因此,编译器认为嵌入的rebind name不是模板name,同时,<>被解释为大于,小于操作。但是,这只是我们问题的开始。就算编译器能够知道rebind 是template name,它也会认为不是type name。因此,必须这么写typedef。
typedef typename A::template rebind::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 (<) 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 (<

下载本文示例代码


What are you, Anyway? What are you, Anyway? What are you, Anyway? What are you, Anyway? What are you, Anyway? What are you, Anyway? What are you, Anyway? What are you, Anyway? What are you, Anyway? What are you, Anyway? What are you, Anyway? What are you, Anyway?
阅读(200) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~