欢迎访问毕业设计网,我们将竭诚为您服务! 今天是:
您现在的位置: 爱毕业设计网 >> 计算机教程 >> 计算机编程 >> 正文
现代的C++设计
来源:2BYSJ.CN 文章编号:2BYSJ2478100

摘要
以下是本书各组件用到的技术,大部分技术都和模块有关。
●编译期,帮助程序库为泛型码产生有意义的错误信息。
●模板偏特化,让你可以特化template —并非针对
特定的、固定集合的参数,而是针对「吻合某个式样的一群参数。
● 区域类别,让你做些有趣的事,特别是对模块函式。
● 常整数映射为型别,允许在编译期以数值特别是布尔数学体系作为分派的取决因素。
● 型别对型别的映射,让你利用函式重载取代C++ 缺乏的一个特性:函式模板偏特化。
● 型别选择,让你得以根据布尔数学体系的条件来选择型别。
● 编译期间侦测可转换性和继承性,让你得以判断任意两型别是否可互相转换,或是否为相同型别,或是否有继承关系。
● 零型和空型,其功能像是在模板原程序中的占位类别。
表格2.1  类型特性中的各个成员包括
名称 种类 说明
isPointer Boolean常数 如果T是指标,此值为True.
PointeeType Tyte 如果T是个指标类别,此式求得T所指型别。如果T不是指标类别,核定结果为零型.
isreference type 如果T是个参考类别,核定结果为true。
referencedtype Type 如果T是个参考类别,此式求得T所指类别。否则求得T自身类别。
parametertype Type 此式求得(最适合做为一个nonmutable函式(译注:不会更改操作对象内容)的参数)的类别。可以是T或const T&。
Isconst  Boolean常数 如果T是个常数类别(经const修饰),则为true。
nonconsttype Type 将类别T的const修词拿掉(如果有的话)。
isvolatile Boolean常数 如果T是个经volatile修饰的类别,则为true。
NonVolatileType Type 将类别T的volatile修辞拿掉(如果有的话)。
NonQulifiedType Type 将类别T的const和volatile修辞拿掉(如有的话)。
isStdUnsignedInt Boolean常数 如果T 是四个不带正负号的整数类(unsignedchar,unsigned short int, unsigned int , unsigned long int),此值为true。
isStdSignedInt Boolean常数 如果T 是四个带正负号的整数类别之㆒(char, short
int,int,long int),此值为true。
isStdIntegral Boolean常 如果T是个标准正数类别,此值为true。
isStdFloat Boolean常 如果T 是个个标准浮点数类别(float,double,long
double),此值为true。
isStdArith Boolean常 如果T 是个标准算术类别(整数或浮点数),此值为true。
isStdFundamental Boolean常 如果T 是个基本类别(算术类别或void),此值为true。
2.1 编译期(Compile-Time)
技术:
本章呈现许多贯穿本书的C++ 技术。为了在各式各样的情境中都有用,他们倾向于泛化(一般化)和可复用,如此便可在其它情境中找出他们的应用。
有些技术如局部模板偏特化是语言本身的特性,有些如编译期则需藉由程序码宝作出来。
本章之中你将了解下列这些技术和工具:
● 模板偏特化
● 局部类
● 型别和数值之间的映射
● 在编译期察觉可转换性和继承性
●资讯类别,以及一个容易上手的std::type_info外复类别
● 分段模块。这是一个工具,可在编译期间根据某个弯曲件状态选择某个类别
● 特性,一堆显著的特点技术集合,可施行于任何C++ 型别身上,如果分开来看,每个技术和其所用之程序码也许都不怎么样;他们都由5~10 行浅显易懂的程序码组成。然而这些技术有一个重要特性:他们没有极限。也就是说,你可以把它们组合成一个高阶惯用手法。当它们合作,便形成一个强大的服务基础,可协助我们建立比较强大的结构。这些技术都带有范例,所以讨论起来并不枯燥。阅读本书其余部分时,也许你会回头参考本章。
2.1 编译期(Compile-Time)
随着泛型编程在C++ 大行其道,更好的静态检验以及更好的可订制型错误讯息的需求浮现了出来。举个例子,假设你发展出一个用来作安全转换的函式。你想将某个型别转为其它型别,而为了确保原始资讯被保留,较大型别不能转型为较小型别。
template <class To, class From>
To safe_reinterpret_cast(From from)
{
assert(sizeof(From) <= sizeof(To));
return reinterpret_cast<To>(from);
}
你可以像运用「C++ 内建之型别转换操作」一样的呼叫上述函式:
int i = ...;
char* p = safe_reinterpret_cast<char*>(i);
你必须明白指定To 这个template 引数;编译器会根据i 的型别推导出另一个模块引数。即由上述的「大小比较」断言动作,便可确定「标的型别」足以容纳「源端型别」的所有比特。如此以来上述程序码便可用到正确的型别转换,或导致一个执行期声明。
很显然,我们都希望错误能够在编译期便被侦测出来。一则因为转型动作可能是你的程序中偶而执行的分支,当你将程序移植到另一个编译器或平台时,你可能不会记住每一个潜在的不可移植部分,于是留下潜伏臭虫,而它可能在用户面前让你出丑。
这里有一道曙光,算式在编译期评估所得结果是个定值常数,这意味你可以利用编译器来作检查。这个想法是传给编译器一个语言构造,如果是非零算式便合法,零算式则非法。于是当你传入一个算式而其值为零时,编译器会发出一个编译期错误。
最简单的方式称为编译期声明,在C 和C++ 语言中都可以良好运做。它依赖一个事实:大小为零的编队是非法的。
#define STATIC_CHECK(expr) { char unnamed[(expr) ? 1 : 0]; }
现在如果你这样写:
template <class To, class From>
To safe_reinterpret_cast(From from)
{
STATIC_CHECK(sizeof(From) <= sizeof(To));
return reinterpret_cast<To>(from);
}
void* somePointer = ;
char c = safe_reinterpret_cast<char>(somePointer);
而如果在你的系统中,指标大小大于字符,编译器会抱怨你「正式着产生一个长度为零的array」。
问题是,你收到的错误讯息无法表达正确资讯。(无法产生一个长度为零的array)这句话无法暗示(char 型别用来持有一个指标实在太小了)。供应是很困难的一件事。错误讯息之间并没有什么必须遵循的规则,端视编译器而定。例如,如果错误讯息指出一个未定变数,该变数名称不一定得出现在错误讯息里面。
较好的解法是依赖一个名称带有意义的template(因为,很幸运地,编译器会在错误讯息中指出template 名称):
template<bool> struct CompileTimeError;
template<> struct CompileTimeError<true> {};
#define STATIC_CHECK(expr) \
(CompileTimeError<(expr) != 0>())
编译期错误需要一个非型别参数(一个bool常数),而且它只针对true有所定义。
如果你试着具现化编译期错误,编译器会发生出 "未下定义的特殊化编译期错误 " 讯息。这个讯息比错误讯息好,因为它是我们故意制造的,不是编译器或程序的臭虫。
当然这其中还有很多改善空间。如何定制错误讯息?我的想法是传入一个额外引数给 STATIC_CHECK,并让它在错误讯息中出现。唯一的缺点是这个定制讯息必须是合法的C++ 辨认符号。这个想法引出了一个改良版编译期错误,如下所示。此后编译期错误之名不再适用,改为编译检验员更具意义:
template<bool> struct CompileTimeChecker
{
CompileTimeChecker(...);};
template<> struct CompileTimeChecker<false> { };
#define STATIC_CHECK(expr, msg)
{\
class ERROR_##msg {}; \ (void)sizeof(CompileTimeChecker<(expr)>(ERROR_##msg()));\
}
假设sizeof(char) < sizeof(void*)
让我们看看当你写出下面这段程序码,会发生什么事:
template <class To, class From>
To safe_reinterpret_cast(From from)
{
STATIC_CHECK(sizeof(From) <= sizeof(To),
Destination_Type_Too_Narrow);
return reinterpret_cast<To>(from);
}
void* somePointer = ...;
char c = safe_reinterpret_cast<char>(somePointer);
宏被处理完毕后,上述的safe_reinterpret_cast会被展开成下列样子:
template <class To, class From>
To safe_reinterpret_cast(From from)
{
{
class ERROR_Destination_Type_Too_Narrow {};
(void)sizeof(
CompileTimeChecker<(sizeof(From) <= sizeof(To))>(
ERROR_Destination_Type_Too_Narrow()));
}
return reinterpret_cast<To>(from);
}
}
这段程序定义了一个名为错误的局部类那是一个空类别。然后生成一个型别为编译检验员<(sizeof(From) <= sizeof(To))>的暂时物,并以一个型别为 错误的局部类 的暂时对象加以初始化。最终,大小会测量出这个出物件的大小。这是个小技巧。编译检验员这个特化有一个可接受任何参数的建构式;它是一个(参数列为简略符号)的函式。这意味如果编译期的算式评估结果为true,这段程序码就有效。如果大小比较结果为false,就会有编译期错误发生:因为编译器找不到ERROR_Destination_Type_Too_Narrow CompileTimeChecker<false>转成 CompileTimeChecker<false>" 的方法。最棒的是编译器能够输出如下正确讯息:"Error: Cannot convert ERROR_Destination_Type _Too_Narrow to CompileTimeChecker<false>”, 这真是太棒了!
2.2 Partial Template Specialization(模板偏特化)
部分模块让你在模块的所有可能实际中特化出一组子集。让我们先扼要解释模块特殊化。如果你有这样一个模块类,名为容器:
template <class Window, class Controller>
class Widget
{generic implementation };
这样明白加以特化:
template <>
class Widget<ModalDialog, MyController>
{specialized implementation };
其中 对话框和 控制器是你另外定义的类。
有了这个Widget特化定义之后,如果你定义Widget<ModalDialog,MyController> 对象,编译器就使用上述定义,如果你定义其它泛型对象,编译器就使用原本的泛型定义。然而有时候你也许想要针对任意窗体并搭配一个特定的控制器来特化Widget。这时就需要PartialTemplate Specialization 机制:
译注: 仍是泛化
template <class Window>//  Window
译注: 是特化
class Widget<Window, MyController>//  MyController
{partially specialized implementation };
通常在一个template类偏特化定义中,你只会特化某些 template参数而留下其它泛化参数。当你在程序中具体实现上述 template 类,编译器会试着着找出最匹配的定义。这个寻找过程十分复杂精细,允许你以富创造的方式来进行偏特化。例如,假设你有一个按钮控件,它有一个模块参数;那么,你不但可以拿任意 Widget 控制器特定来特化Widget, 还可以拿按钮搭配特定控制器来偏特化Widget:
template <class ButtonArg>
class Widget<Button<ButtonArg>, MyController>
{
... further specialized implementation ...
};
如你所见,偏特化的能力十分令人惊讶。当你具现化一个 template时,编译器会把目前存在的偏特化和全特化templates作比较,并找出其中最合适者。这样的机制给了我们很大弹性。不幸的是偏特化机制不能用在函式身上(不论成员函式或非成员函式),这样多少会降低一些你所能作出来的弹性和粒。
● 虽然你可以全特化class template中的成员函式,但你不能偏特化它们。
● 你不能偏特化namespace-level函式。最接偏特化机制的是函式重载 — 就实际运用而言,那意味你对「函式参数」(而非回返值型别或内部所用型别)有很精致的特化能力。例如:
template <class T, class U> T Fun(U obj); // primary template
template <class T> T Fun (Window obj); // legal (overloading)
如果没有偏特化,编译器设计者的日子肯定会好过一些,但却对程序开发者造成不好的影响。稍后介绍的一些工具都呈现偏特化的极限。本书频繁运用偏特类型表化的所有设施几乎都建立在这个机制上。
2.3区域类别(Local Classes)
这是一个有趣而少人知道的C++ 特性。你可以在函式中定义类 ,像下面这样:
void Fun()
{
class Local
{
member variables
member function definitions};
code using Local
}
不过还是有些限制,局部类不能定义静态成员变数,也不能存非静止区域变数。局部类令人感兴趣的是,可以在template 函式中被使用。定义于template 函式内的局部类可以运用函式的template
 参数。以下所列程序码中有一个适配器模块功能设备,可以将某个接口转接为另一个界面。适配器制造者在其局部类的协助下实作出一个界面。这个 局部类 内有泛化型别的成员。
class Interface
{
public:
virtual void Fun() = 0;
};
template <class T, class P>
Interface* MakeAdapter(const T& obj, const P
{
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);
}
事实证明,任何运用局部类的手法,都可以改用「函式外的 template classes」来完成。
换言之并非一定得局部类不可。不过局部类可以简化实作并提高符号的地域性。局部类 倒是有个独特性质:她们是(也即Java 口中的final)。外界不能,你必须在编译继承一个隐藏于函式内的 class。如果没有局部类 ,为了实现JAVA final,单元中加上一个无具名的命名空间。我将在运用 局部类产生所谓的函式。
2.4常整数映射为型别(Mapping Integral Constants to Types)
下面是最初由 Alexandrescu提出的一个简单template,对许多泛型编程手法很有帮助:
template <int v>
struct Int2Type
{
enum { value = v };
};
Int2Type会根据引数所得的不同数值来产生不同型别。这是因为(不同template 具现体)本身便是(不同的型别)。因此Int2Type<0>不同于 Int2Type<1>,以此类推。用来产生型别的那个数值是一个列举元。
当你想把常数视同型别,便可采用上述的Int2Type。这么一来便可根据编译期计算出来的结果选用不同的函式。实际上你可以运用一个常数运到静态分功能。
一般而言,符合下列两个条件便可使用Int2Type:
●有必要根据某个编译期常数呼叫一个或数个不同的函式。
●有必要在编译期实施分派。
如果打算在执行期进行分派,可使用if-else或 switch述句。大部分时候其执行期成本都微不足道。然而你还是无法常常那么做,因为if-else述句要求每一个分支都得编译成功,即使该条件测试在编译期才知道。困惑了吗?读下去!
假想你设计出一个泛形容器NiftyContainer,它将元素型别参数化:
template <class T> class NiftyContainer
{
...
};
现在假设NiftyContainer 内含指标,指向型别为 T的物件。为了复制NiftyContainer 里面的某个对象,你想呼叫其copy 建构式或虚拟函式 Clone()(针对polymorphic 型别)。你以一个 boolean template参数取得使用者所提供的资讯:
template <typename T, bool isPolymorphic>
class NiftyContainer
{
void DoSomething()
{
T* pSomeObj = ...;
if (
isPolymorphic)
{
T* pNewObj = pSomeObj->Clone();
... polymorphic algorithm ... (多型算法)
}
else
{
T* pNewObj = new T(*pSomeObj);
... non-polymorphic algorithm ...
}
}
};
};
问题是,编译器不会让你侥幸成功。如果多型算法使用pObj->Clone() ,那么面对任何一个未曾定义成员函式Clone()之型别NiftyContainer::DoSomething()都无法编译成功。虽然编译期间很容易知道哪一条分支会被执行起来,但这和编译器无关,因为即使最佳化工具可以评估出哪一条分支不会被执行,编译器还是会勤劳地编译每个分支。如果你呼叫 NiftyContainer<int,false> DoSomething() pObj->Clone(),编译器会停止。

上述的non-polymorphic 部分也有可能编译失败。如果T是个polymorphic型别,而上述的non-polymorphic程序分支想作new T(*pObj)动作,这样也有可能编译失败。举个实例,如果T借着(把copy建构式至于private 区域以产生隐藏效果),就像一个有良好设计的polymorphic class 那样,那么便有可能发生上述的失败情况。
如果编译器不去理会那个不可能被执行的程序码就好了,然而目前情况下是不可能的。什么才是令人满意的解决方案呢?

事实证明有很多解法,而Int2Type提供了一个特别明确的方案。它可以把isPolymorphic这个型别的true 和false转换成两个可资区别的不同型别。然后程序中便可以运用Int2Type<isPolymorphic>进行函式重载。瞧,可不是吗!

  template <typename T, bool isPolymorphic>
class NiftyContainer
{
private:
void DoSomething(T* pObj, Int2Type<true>)
{
T* pNewObj = pObj->Clone();
... polymorphic algorithm ...
}
void DoSomething(T* pObj, Int2Type<false>)
{
T* pNewObj = new T(*pObj);
... nonpolymorphic algorithm ...
}
public:
void DoSomething(T* pObj)
{
DoSomething(pObj, Int2Type<isPolymorphic>());
}
};
};
Int2Type是一个用来「将数值转换为型别」的方便手法。有了它,你便可以将该型别的一个暂时对象传给一个重载函式(overloaded function ),后者实现现必要的算法。(译注:这种手法在STL 中亦有大量实现,唯形式略有不同;详见 STL源码,或《STL源码剖析》by 侯捷)这个小技巧之所以有效,最主要的原因是,编译器并不会去编译一个未被用到的template 函式,只会对它做文法检查。至于此技巧之所以有用,则是因为在template 程序码中大部分情形下你需要在编译期作流程分派(dispatch)动作。
你会在Loki的数个地方看到Int2Type的运用,尤其是本书Multimethods 。在那儿,template class是一个双分派( double-dispatch)引擎,运用bool template参数决定是否要支持对称性分派( symmetric dispatch)。
2.5型别对型别的映射(Type-to-Type Mapping)
就如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()在任何泛型程序中不再有用。
由于你无法偏特化一个函式,因此无法写出下面这样的程序码:
template <class U>
Widget* Create<Widget, U>(const U& arg)
{
return new Widget(arg, -1);
}
}
由于函式缺乏偏特化机制,因此(再一次地)你只有一样工具可用:多载化(重载)机制。我们可以传入一个型别为T的暂时物件,并以此进行重载:
template <class T, class U>
T /* dummy */)
T* Create(const U& arg,
{
return new T(arg);
}
template <class U>
Widget /* dummy */)
Widget* Create(const U& arg,
{
return new Widget(arg, -1);
}
}
这种解法会轻易建构未被使用的复杂对象,造成额外开销。我们需要一个轻量级机制来传送「型别T的资讯」到 Create()中。这正是Type2Type扮演的角色,它是一个型别代表物,一个可以让你传给多载化函式的轻量级ID 。Type2Type定义如下:
template <typename T>
struct Type2Type
{
typedef T OriginalType;
};
它没有任何数值,但其不同型别却足以区分各个 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);
}
String* pStr = Create("Hello", Type2Type<String>());
Widget* pW = Create(100, Type2Type<Widget>());
Create()的第二参数只是用来选择适当的重载函式,现在你可以令各Type2Type 实体对应于你的程序中的各种型别,并根据不同的Type2Type实体来特化Create() 。
2.6型别选择(Type Selection )
有时候,泛型程序需要根据一个boolean变量来选择某个型别或另一型别。
2.4节讨论的NiftyContainer例子中,你也许会以一个std::vector 作为后端储存结构。很显然,面对polymorphic(多型)型别,你不能储存其对象实体,必须储存其指针。但如果面对的是non-polymorphic(非多型)型别,你可以储存其实体,因为这样比较有效率。
在你的 class template中:
template <typename T, bool isPolymorphic>
class NiftyContainer
{
};
你需要存放一个vector<T*> (如果isPolymorphic 为true)或vector<T>(如果
isPolymorphic为 false)。根本而言,你需要根据isPolymorphic来决定将ValueType定义为T*或T。你可以使用traits class template Alexandrescu 2000a
( )如下:
template <typename T, bool isPolymorphic>
struct NiftyContainerValueTraits
{
T* ValueType;
typedef
};
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 T, typename U>
struct Select<false, T, U>
{
typedef U Result;
};
其运作方式是:如果 flag为 true,编译器会使用第一份泛型定义式,因此Result
会被定义成T。如果 flag为false ,那么偏特化机制会进场运作,于是Result 被定义成 U。
现在你可以更方便地定义NiftyContainer::ValueType了:
template <typename T, bool isPolymorphic>
class NiftyContainer
{
typedef Select<isPolymorphic, T*, T>::Result   ValueType;
};
2.7编译期间侦测可转换性(Convertibility)和继承性( Inheritance)
实作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会丢弃算式并传回其大小 。
「侦测转换能力」的想法是:合并运用sizeof和重载函式。我们提供两个重载函式:其中一个接受U (U 型别代表目前讨论中的转换标的」,另一个接受「任何其它型别」。我们以型别T的暂时对象来唤起这些重载函式,而「T 是否可转换为U 」正是我们想知道的。如果接受U的那个函式被唤起,我们就知道T 可转换为U ;否则U 便无法转换为T 。为了知道哪一个函式来区分其大小。被唤起,我们对这两个重载函式安排大小不同的回返型别,并以sizeof型别本身无关紧要,重要的是其大小必须不同。
让我们先建立两个不同大小的型别。很显然char 和long double的大小不同,不过C++ 标准规格书并未保证此事,所以我想到一个极其简单的作法:
typedef char Small;
class Big { char dummy[2]; };
根据定义sizeof(Small)是1 。而Big的大小肯定比 1还大,这正是我们所需要的保证。接下来需要两个重载函式,其一如先前所说,接受一个U对象并传回一个 Small物件:
Small Test(U);
但,接下来,我该如何写出一个可接受任何其它种对象的函式呢?template
 并非解决之道,因为template总是要求最佳匹配条件,因而遮蔽了转换动作。我需要一个「比自动转换稍差」的匹配,也就是说我需要一个「唯有在自动转换缺席情况下」才会雀屏中选的转换。我很快看了一下施行于函式呼叫的各种转换规则,然后发现所谓的「简略符号比对」准则,那是最坏的情况了,位于整个列表的最底端。于是我写出这样一个函式:
Big Test( );
(呼叫一个带有简略符号的函式并传入一个C++ 对象,无人知道结果会如何。不过没关系,我们并不真正呼叫这个函式,它甚至没被实作出来。还记得吗,sizeof
并不对其自变量求值)现在我们传一个T对象给Test()  ,并将sizeof施行于其传回值身上:
const bool convExists = sizeof(Test(T())) == sizeof(Small);
default
就是这样! Test()会取得一个T() 建构对象 ,然后sizeof会取得此一算式结果的大小,可能是sizeof(Small) 或sizeof(Big) ,取决于编译器是否找到转换方式。

这里还有一个小问题。万一T让自己的 default建构式为private ,那么 T()会编译失败,我们所有的舞台支架也将倒塌。庆幸的是有一个简单解法:以一个「稻草人函式」(strawman function)传回一个T物件。还记得吗,我们处于sizeof的神奇世界中,并不会真有任何算式被求值( evaluated)。本例之中编译器高兴,我们也高兴。
T MakeT(); // not implemented
const bool convExists = sizeof(Test(MakeT())) == sizeof(Small);
(顺便提一下,像MakeT() 和Test()这样的函式,你能在它们身上做多少事情?它们不只没做任何事情,甚至根本不真正存在。这不是很好玩吗?)现在让它运作,把所有东西以class template包装起来,隐藏「型别推导」的所有细节,只曝露结果:
template <class T, class U>
class Conversion
{
typedef char Small;
class Big { char dummy[2]; };
static Small Test(U);
static Big Test(...);
static T MakeT();
public:
enum { exists =
sizeof(Test(MakeT())) == sizeof(Small) };
};
下面程序代码用来测试上述的Conversion class template:
int main()
{
using namespace std;
cout << Conversion<double, int>::exists << ' '
<< Conversion<char, char*>::exists << ' '
<< Conversion<size_t, vector<int> >::exists << ' ';
}
这个小程序会印出"100"。注意,虽然std::vector 实作出一个接受size_t
自变量的建构式,但上述转换测试却传回0 ,因为该建构式是explicit (译注:explicit建构式无法担任转换函式)我们在Conversion 中更实作出两个常数:
●exists2Way,表示T 和 U之间是否可以双向转换。例如 int和double可以双向转换。使用者自定型别也可以实作出这样的转换。
●sameType;如果 T和 U 是相同型别,这个值便为true。
template <class T, class U>
class Conversion
{
... as above ...
enum { exists2Way = exists &&
Conversion<U, T>::exists };
enum { sameType = false };
};
partial specialization
Conversion
sameType
我们也可以透过 的偏特化( )来实作出 :
template <class T>
class Conversion<T, T>
{public:
enum { exists = 1, exists2Way = 1, sameType = 1 };
};
最后,让我们回头看看,有了Conversion的帮助,要决定两个classes之间是否存在继承关系,变得很容易:
#define SUPERSUBCLASS(T, U) \
(Conversion<const U*, const T*>::exists && \
!Conversion<const T*, const void*>::sameType)
如果U 是public继承自 T,或U和 T是同一型别,那么SUPERSUBCLASS(T,U) 会传回true。当SUPERSUBCLASS(T, U) 对const U*  和const T*  作「可转换性」评估时,只有三种情况下const U*可以隐式转换为 const T*:
1. T和U是同一种型别。
2. T是U的一个unambiguous (不模棱两可的、非歧义的)public base。
3. T是 void。
第三种情况可以在前述第二次测试中解决掉。如果把第一种情况(T和U是同一型别)视为is-a的退化,实作时会很有用,因为实用场合中你常常可以将某个 class视为它自己的 superclass。如果你需要更严谨的测试,可以这么写:
#define SUPERSUBCLASS_STRICT(T, U) \
(SUPERSUBCLASS(T, U) && \
!Conversion<const T, const U>::sameType)
为何这些程序代码都加上const 饰词?原因是我们不希望因 const而导致转型失败。如果template程序代码实施const两次(对一个已经是const的型别而言),第二个const 会被忽略。简单地说,藉由在SUPERSUBCLASS中使用const,我们得以更安全一些。
为甚么选用SUPERSUBCLASS而不是更可爱的名称如BASE_OF 或INHERITS之类?这是基于一个非常实际的理由。一开始Loki对它的命名是INHERITS ,但是当INHERITS(T,U)运作时,出现了一个问题:它说的是T继承 U或相反呢?改名为SUPERSUBCLASS(T,U)之后,谁先谁后就变得很清楚。
2.8 NullType和EmptyType
Loki定义了两个非常简单的型别:零型和空型。你可以拿它们当作型别计算时的某种边界标记。
零型是一个只有宣告而无定义的 class:
Class NullType; // no definition
你不能生成一个零型对象,它只被用来表示「我不是个令人感兴趣的型别」。 节把零型用在有语法需求却无语意概念的地方(例如「int 指的是什么型别」)。类型表t以零型标记类型表 的末端,并用以传回「找不到型别」这一讯息。
第二个辅助型别是EmptyType 。和你想的一样,空型定义如下:
struct EmptyType {};
这是一个可被继承的合法型别,而且你可以传递空型对象。你可以把这个轻量级型别视为template 的预设(可不理会的)参数型别。类型表就是这样用它。
书名:<< Modern C++ Design >>
作者:ALEXANDRESCU, ANDREI
出版社:ADDISON-WESLEY

您可能还对以下教程感兴趣:
没有相关教程