欢迎访问毕业设计网,我们将竭诚为您服务! 今天是:
您现在的位置: 爱毕业设计网 >> 计算机教程 >> 计算机编程 >> 正文
现代的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