2.3 局部类(Local Classes)
这是一个有趣而少有人知道的C++特性。你可以在函数中定义class,像下面这样:
void Fun ( ){ class Local{ .. . member variables ... ... member function definitions ... }; .. code using Local ... }
不过还是有些限制:
- local class不能定义static成员变量 (不能定义静态成员变量)
- 也不能访问non-static局部变量。(非静态局部变量)
local classes令人感兴趣的是,可以在template函数中被使用。定义于template函数内的 local classes可以运用函数的template参数。以下所列代码中有一个MakeAdapter template function,可以将某个接口转接为另一个接口.MakeAdaptery在其local class的协助下实作出一个接口。这个localclass内有泛化型别的成员。
class Interface { public: virtual void Fun ( ,=0; }; template <class T, class P> Interface* MakeAdapter (const T& obj, const P& arg)( class Local : public Interface{ public: Local (const T& obj, const P& arg) : obj_(obj) , arg_ ( arg){ }virtual void Fun ( ) { obj_.call (arg_) ; ) private : T obj_;P arg_; }; return new Local ( obj, arg ) ; }
事实证明,任何运用local classes 的手法,都可以改用“函数外的template classes”来完成。换言之,并非一定得local classes不可。
不过 local classes可以简化实作并提高符号的地域性。Local classes 倒是有个独特性质:它们是“最后一版”(也即 Java口中的final)。外界不能继承一个隐藏于函数内的 class。如果没有local classes,为了实现Java final,你必须在编译单元(译注:也就是个别文件)中加上一个无具名的命名空间(namespace)。
我将在第11章运用local classes 产生所谓的“弹簧垫”函数(trampoline functions)。
2.4 常整数 映射为 型别( Mapping Integral Constants to Types)
下面是最初由Alexandrescu ( 2000b)提出的一个简单template,对许多泛型编程手法很有帮助:
template <int v> struct Int2Type { enum {value = v } ; } ;
Int2Type会根据引数所得的不同数值来产生不同型别。这是因为“不同的template具现体”本身便是“不同的型别”。因此Int2Type<0>不同于Int2Type<1>,以此类推。用来产生型别的那个数值是一个枚举值( enumerator)。
当你想把常数视同型别时,便可采用上述的Int2Type。这么一来便可根据编译期计算出来的结果选用不同的函数。实际上你可以运用一个常数达到静态分派(static dispatching) 功能。
一般而言,符合下列两个条件便可使用Int2Type:
- 有必要根据某个编译期常数调用一个或数个不同的函数。
- 有必要在编译期实施“分派”( dispatch)。
如果打算在执行期进行分派(dispatch),可使用if-else或switch语句。大部分时候其执行期成本都微不足道。然而你还是无法常常那么做,因为 if-else语句要求每一个分支都得编译成功,即使该条件测试在编译期才知道。困惑了吗?读下去!
假想你设计出…个泛形容器Niftycontainer,它将元素型别参数化:
template <class T> class NiftyContainer { ... }
现在假设Niftycontainer内含指针,指向型别为T的对象。为了复制Niftycontainer里的某个对象,你想调用其 copy构造函数(针对non-polymorphic型别)或虚函数clone ()(针对polymorphic型别)。你以-一个boolean template参数取得使用者所提供的信息:
tempiate <typename T, bool isPolymorphic> class NiftyContainer { void DoSomething () { T* pSomeObj = .. .; if(1sPo1 ymorphic){ *pNewObj = psomeObj->Clone () ; ... polymorphic algorithm ...(多态算法) } else{ *pNewObj = new T ( *pSome0bj ) ;//译注:copy构造函数 ... non-polymorphic algorithm ... (非多态算法) } } };
问题是,编译器不会让你侥幸成功。如果多态算法使用pobj->Clone ),那么面对任何一个未曾定义成员函数clone ()之型别,NiftyContainer: : DoSomething ()都无法编译成功。虽然编译期间很容易知道哪一条分支会被执行起来,但这和编译器无关,因为即使优化工具可以评估出哪一条分支不会被执行,编译器还是会勤劳地编译每个分支。如果你试着调用Niftycontainer<int,false>的 DoSomething ),编译器会停在p0bj->clone ( )处并说“嗨,不行哨”。
上述的non-polymorphic部分也有可能编译失败。如果T是个 polymorphic型别,而上述的non-polymorphic程序分支想做newT( *pObj)动作,这样也有可能编译失败。举个实例,如果T借着“把 copy构造函数置于private区域以产生隐藏效果”,就像一个有良好设计的polymorphicclass那样,那么便有可能出现上述的失败情况。
如果编译器不去理会那个不可能被执行的代码就好了,然而目前情况下是不可能的。甚么才是令人满意的解决方案呢?
事实证明有很多解法,而Int2Type提供了一个特别明确的方案。它可以把isPolymorphic这个型别的 true和 false 转换成两个可资区别的不同型别,然后在程序中便可以运用Int2Type进行函数重载。瞧,可不是吗!
template <typename T, bool 1sPo1ymorphicclass Ni ftyContainer { private: void Dosomething ( T* pobj, Int2Type<true>) { T*pNewObj = pObj->Clone ( );... polymorphic algorithm . } void Dosomething (T*pObj, int2Type<false>){ T* pNewobj = new T ( *pobj ) ;... nonpo1 ymorphic algorithm . } public: void Dosomething ( r* pObj){ Dosomething (pobj, Int2Type<isPolymorphic> () ); } };
Int2Type是一个用来“将数值转换为型别”的方便手法。有了它,你便可以将该型别的一个暂时对象传给一个重载函数(overloaded function),后者实现必要的算法。
(译注:这种手法在STL中亦有大量实现,唯形式略有不同;详见STL源码,或《STL源码剖析》by 侯捷
这个小技巧之所以有效,最主要的原因是,编译器并不会去编译一个未被用到的template函数,只会对它做文法检查。至于此技巧之所以有用,则是因为在 template 代码中大部分情形下你需要在编译期做流程分派(dispatch)动作。
你会在Loki的数个地方看到Int2Type的运用,尤其是本书第11章:Muttimethods。
在那儿,template class是个 双分派(double-dispatch)引擎,运用bool template参数决定是否要支持对称性分派(symmetric dispatch) 。
2.5 类别对类别的映射
就如2.2节所说,并不存在template函数的偏特化。然而偶尔我们需要模拟出类似机制。
试想下面的程序:
template <class T, class U> T* Create (const U& arg) { return new T ( arg ) ; }
create( )会将其参数传给构造函数,用以产生一个新对象。
现在假设你的程序有个规则: widget对象是你碰触不到的老旧代码,它需要两个引数才能构造出对象来,第二引数固定为-1。widget派生类则没有这个问题。
现在你该如何特化Create (),使它能够独特地处理widget呢?一个明显的方案是另写出一个CreateWidget()来专门处理。但这么一来你就没有一个统一的接口用来生成widgets和其派生对象。这会使得create ()在任何泛型程序中不再有用。
由于你无法偏特化一个函数,因此无法写出下面这样的代码:
//llegal code — don 't try this at home template <class U> Widget* Create<Widget, U> (const U& arg) { return new widget (arg,一1); }
由于函数缺乏偏特化机制,因此(再一次地)你只有一样工具可用:
- 多载化(重载)机制。
我们可以传入一个型别为T的暂时对象,并以此进行重载:
template <class T , class U> T* Create (const U&arg, T /*dummy */) { return new T ( arg) ; } template <class U> Widget* Create (const U& arg,Widget /*dummy*/) { return new Widget (arg,-1) ; )
这种解法会轻意构造未被使用的复杂对象,造成额外开销。我们需要个轻量级机制来传递“型别T的信息”到Create()中。这正是Type2Type扮演的角色,它是一个型别代表物,一个可以让你传给重载函数的轻量级ID。Type2Type定义如下:
template <typename T> struct Type2Type { typedef T OriginalType; };
它没有任何数值,但其不同型别却足以区分各个 Type2Type实体,这正是我们所要的。现在你可以这么写:
// An implementation of Create relying on overloadinglland Type2Type template <class T, class U> T* Create (const U&arg, Type2Type<T>) { return new T (arg) ; } template <class U> Widget* Create(const U& arg, Type2Type<Widget>) { return new widget (arg,-1); } // Use Create ( ) String* pStr = Create("Hello",Type2Type<String> ()); Widget* pW - Create(100,Type2Type<Widget>());
Create()的第二参数只是用来选择适当的重载函数,现在你可以令各种 Type2Type实体对应于你的程序中的各种型别,并根据不同的 Type2Type实体来特化Create ( )。
2.6 类别的选择
有时候,泛型程序需要根据一个boolean变量来选择某个型别或另一型别。
在2.4节讨论的NiftyContainer例子中,你也许会以一个std::vector作为后端存储结构。很显然,面对 Polymorphic((多态〉型别,你不能存储其对象实体,必须存储其指针。但如果面对的是non-polymorphic(非多态)型别,你可以存储其实体,因为这样比较有效率,在你的class template 中:
template <typename T, bool isPolymorphic> class Niftycontainer { ... }
你需要存放一个vector<T*>(如果 isPolymorphic 为true)或vector(如果isPolymorphic为 false)。根本而言,你需要根据isPolymorphic来决定将valueType定义为T*或T。你可以使用traits class template ( Alexandrescu 2000a)如下:
template <typename T, bool isPo1ymorphic> struct NiftyContainerValueTraits { typedef T* ValueType; }; template <typename T> struct NiftyContainerValueTraits<T, false> { typedef T ValueType; }; template <typename T, bool isPolymorphic> class NiftyContainer { ... typedef NiftyContainerValueTraits<T, isPolymorphic> Traits; typedef typename Traits::ValueType valueType; };
这样的做法其实笨拙难用,此外它也无法扩充:针对不同的型别的选择,你都必须定义出专属的 traits class template。
Loki提供的 Select Class template可使型别的选择立时可用。它采用偏特化机制(Partial Template Specialization) :
template <bool flag, typename T, typename u>struct select { typedef T Result; ) ; template <typename r, typename u>struct select<false, T ,U> { typedef U Result; } ;
2.7 便器期间侦测可转换性和继承性
实作 template functions 和 template classes时我常常发现一个问题:面对两个陌生的型别T和U,如何知道U是否继承自T呢? 在编译期间发掘这样的关系,实在是实作泛型程序库的一个优化关键。在泛型函数中,如果你确知某个class实作有某个接口,你便可以采用某个最佳算法。在编译期发现这样的关系,意味着不必使用dynamic_cast——它会耗损执行期效率。
发掘继承关系,靠的是一个用来侦测可转换性(convertibility)的更一般化机制。这里我们面临更一般化的问题:
如何测知任意型别T是否可以自动转换为型别U?
有个方案可以解决问题,并且只需依赖sizeof。
sizeof有着惊人的威力:你可以把sizeof用在任何表达式 (expression)身上,不论后者有多复杂。sizeof 会直接传回大小,不需拖到执行期才评估。这意味着sizeof 可以
- 感知重载(overloading)、
- 模板具现( template instantiation)、
- 转换规则(conversion rules),
或任何可发生于C+表达式身上的机制。
事实上sizeof背后隐藏了一个“用以推导表达式型别”的完整设施。最终sizeof 会丢弃表达式并传回其大小1。
“侦测转换能力”的想法是:合并运用sizeof 和重载函数。我们提供两个重载函数:其中一个接受U(U型别代表目前讨论中的转换目标),另一个接受“任何其他型别”。我们以型别T的暂时对象来调用这些重载函数,而“T是否可转换为U”正是我们想知道的。如果接受U的那个函数被调用,我们就知道T可转换为U;否则T便无法转换为U。
为了知道哪一个函数被调用,我们对这两个重载函数安排大小不同的返回型别,并以 sizeof来区分其大小。型别本身无关紧要,重要的是其大小必须不同。
让我们先建立两个不同大小的型别。很显然char和 long double的大小不同,不过C++标准规格书并未保证此事,所以我想到一个极其简单的做法:
typedef char Small; class Big { char dummy [2]; } ;
2.8 type_info的一个外覆类
std::type_info class
标准C++提供了一个std::type_info class,使你能够于执行期间查询对象型别。
外覆类
2.9 NullType和EmptyType
Loki 定义了两个非常简单的型别: NullType和 EmptyType。你可以拿它们当做型别计算时的某种边界标记。
NullType
NullType是一个只有声明而无定义的class:
class NullType;// no definition
你不能生成一个NullType对象,它只被用来表示“我不是个令人感兴趣的型别”。2.10节把NullType用在有语法需求却无语义概念的地方(例如“int 指的是什么型别”)。第3章的 typelist以NullType标记typelist的末端,并用以传回“找不到型别”这一消息。
EmptyType
第二个辅助型别是EmptyType。和你想的一样,EmptyType定义如下:
struct EmptyType i } ;
这是一个可被继承的合法型别,而且.你可以传递EmptyType对象。你可以把这个轻量级型别视为template的缺省(可不理会的)参数型别。第3章的 typelist就是这样用它的。
2.10 Type Traits
前言
Traits是一种“可于编译期根据型别作判断”的泛型技术,很像你在执行期根据数值进行判断一样(Alexandrescu 2000a)。众所皆知,加上一个间接层便可解决很多工程问题,trait让你得以在“型别确立当时”以外的其他地点做出与型别相关的判断。这会让最终的代码变得比较干净,更具可读性,而且更好维护。
通常,当你的泛型程序需要时,你会写出自己的trait templates 和 trait classes。然而某些traits可应用于任何型别,它们可以帮助泛型程序员根据型别特性修改出适当的泛型代码。
template <typename InIt, typename OutIt > OutIt Copy (InIt first, InIt last,OutIt result) { for (; first != last ; ++first, ++result ) *result * first ; }
std::copy函数
2.10.1 实作出PointerTraits
2.10.2 侦测基本类别
2.10.3 优化的参数类别
2.10.4 卸除饰词
2.10.5运用TypeTraits
2.10.6 包装
2.11 摘要
总结一
模板几个作用:
- 静态检测 2.1节
- 静态重载,选择合适的分支 2.4节
总结二
函数虽然无法使用模板偏特化,但是可以使用重载 2.4节
- 目前正有一份提案,准备为C++语言加入typeof操作符,它会传回一个表达式的型别。有了这个typeof操作符,泛型程序将会更好写,也更易被了解。Gnu C++己实作出typeof作为语言扩充功能。很显然typeof和sizeof拥有共同的后端实作,因为 sizeof也需要判断型别