分享
 
 
 

What are you, Anyway

王朝vc·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

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>

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有