前言
本章呈现许多贯穿木书的C++技术。
为了在式各样的情境(context)中都有用,它们倾向于:
- 泛化(一般化,general)
- 可复用(reusable),
如此便可在其他情境中找出它们的应用。
有些技术如 partial template specialization(模板偏特化)是语言本身的特性,有些如编译期assertions"则需藉由代码实作出来。
- Partial template specialization(模板偏特化)
- Local classes(局部类)
- 型别和数值之间的映射( Int2Type 和l Type2Type class templates)
- 在编译期察觉可转换性( convertibility)和继承性
- 型别信息,以及一个容易上手的std::type_info外覆类( wrapper)
- select class template。这是一个工具,可在编译期间根据某个bool状态选择某个型别
- Traits,一堆 traits技术集舍,可施行于任何C++型别身上
如果分开来看,每个技术和其所用之代码也许都不怎么样:它们都由5~10行浅显易懂的代码组成。然而这些技术有一个重要特性:它们没有极限。也就是说,你可以把它们组合成一个高阶惯用手法( idioms)。当它们合作时,便形成一个强壮的服务基础,可协助我们建立比较强壮的结构。
这些技术都带有范例,所以讨论起来并不枯燥。阅读本书其余部分时,也许你会回头参考本章。
2.1 编译期( compile-Time) Assertions
随着泛型编程在C++大行其道,
- 更好的静态检验(static checking)
- 以及更好的可定制型错误消息( customizable error messages)
需求浮现了出来。
举个例子,假设你发展出一个用来作安全转型( safe casting〉的函数。你想将某个型别转为其他型别,i而为了确保原始信息被保留,较大型别不能转型为较小型别。
安全转型( safe casting〉实现
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的型别推导出另一个template引数Fromo藉由上述的“大小比较”assertion动作,便可确定“目标型别”足以容纳“源端型别”的所有bits。如此来,上述代码便可达到正确的型别转换,或导致一个执行期assertion。
错误最好能够在编译期便被侦测出来
错误最好能够在编译期便被侦测出来,原因是因为:
- 移植到另一个平台时,转型操作可能只是一个分支,于是可能留下bug。
解决办法是,表达式在编译期检查所得结果是个定值,这意味着你可以利用编译器来作检查。传给编译器一个表达式,如果是非零表达式便合法,零表达式则非法。于是当你传入一个表达式而其值为零时,编译器会发出一个编译期错误的信息。
最简单的方式称为编译时断言( Compile-Time-Assertions (Van Horn 1997))
2.1.1 方法一 (基本方法,使用声明大小为0数组是非法)
#define STATIC_CHECK (expr) { char unnamed [ (expr) ? 1 : 0]; }
大小为零的array是非法的,所以当表达式为0时,
编译器会报错:error C2466: 不能分配常量大小为 0 的数组
缺陷:错误信息无法表示正确信息
2.1.2 方法二 (改进1 运用模板)
#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是非法的,所以当表达式为0时,
编译器会报错:error C2466: 不能分配常量大小为 0 的数组
缺陷:错误信息无法表示正确信息
2.1.3 方法三 (改进2 传入错误信息)
为了提示错误信息,更好的办法是,依赖一个名称带有意义的template (因为,编译器会在错误消息中指template名称):
template<bool> struct CompileTimeError; template<> struct CompileTimeError<true> {}; #define STATIC_CHECK(expr) \ (CompileTimeError<(expr)!=0>())
ComplieTimeError只会针对true有定义。false会报错“Undefined specialization CompileTimeError”
如果你试着调用compileTimeError,编译器会发出"Undefined specializationCompileTimeError”消息。这会在编译器提示我们表达式存在问题。
2.1.4 方法四 (改进3 自定义错误消息)
方法三有改善空间,比如如何定制错误消息?
我的想法是:传入一个额外引数给STATIC_CHECK,并让它在错误消息中出现。
唯一的缺点是:这个定制消息必须是合法的C++标识符(不能间杂空白、不能以数字开头…)。
这个想法引出了一个改良版CompileTimeError如下所示。此后CompileTimeError之名不再适用,改为CompileTimeChecker更具意义:
template<bool> struct CompileTimeChecker { compileTimeChecker ( ... );//译注:这是c/C++支持的非定量任意参数表 }; template<> struct CompileTimeChecker<false> { }; #define sTATIC_CHECK (expr, msg)\ { \ class ERROR_##msg {}; \ //译注:##是个罕被使用的C++操作符 (void)sizeof(CompileTimeChecker<(expr)> (ERROR_##msg ())); \ }
##是一种宏方式的拼接字符串的方法,类似”ERROR_“ + msg
实际使用
假设sizeof(char)< sizeof (void*)(注意,C++ Standard并不保证这一定为真)。让我们看看当你写出下面这段代码,会发生什么事:
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_rype_Too_Narrow { } ; (void) sizeof ( CompileTimeChecker<(sizeof(From)<= sizeof(To))>( ERROR_Destination_Type_Too_Narrow () ) ) ; } return reinterpret_cast<To> (From) ;
这段程序定义了一个名为ERROR_Destination_Type_Too_Narrow 的 local
class,那是一个空类,然后生成一个型别为compileTimeChecker<(sizeof (From) <= sizeof(To))>的暂时对象,并以一个型别为ERROR_Destination_Type_Too.Narrow的暂时对象加以初始化。最终,sizeof会测量出这个对象的大小。
使用模板判断表达式 小技巧
template<> struct CompileTimeChecker<false> { };
这是个小技巧。
compileTimechecker这个特化体有一个可接受任何参数的构造函数;它是一个“参数表为省略符(ellipsis)”的函数。
- 这意味着如果编译期的表达式评估结果为true,这段代码就有效。
- 如果大小比较结果为false,就会有编译期错误发生:
因为编译器找不到将ERRORDestination_Type_Too_Narrow转成CompileTimechecker的方法。
最棒的是编译器能够输出如下正确消息:
“Error: Cannot Convert ERROR_Destination_Type_Too_Narrow to compileTimechecker”
这就能发现报错了。
总结
本章主要就是将运用编译期的错误检查帮助我们立刻解决一些表达式判断问题,尽可能在编译时就发现问题。
所以设计了Static_Check这种宏方式+模板+宏拼接创建空类的方式 自定义出错误,能立刻发现自己报错代码。
报错问题
CompileTimeChecker<false> (safe_reinterpret_cast::ERROR_Destination_Type_Too_Narrow (__cdecl *)(void))”: 非法的 sizeof 操作数 Project1 c:\users\11136\source\repos\project1\project1\main9.cpp 23
其中编译一直报错问题 参考
源码
#include <stdio.h> #include <stdlib.h> //2.1.1、方法一 (基本方法,使用声明大小为0数组是非法) //#define STATIC_CHECK(expr) { char unnamed[(expr) ? 1 : 0]; } //2.1.2 方法二 (改进1 运用模板) //#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); //} //2.1.3 方法三 (改进2 传入错误信息) //template < bool >struct CompileTimeError; // //template < >struct CompileTimeError< true > { }; //#define STATIC_CHECK(expr) (CompileTimeError < ((expr) != 0) >()) //2.1.4、改进3 自定义错误消息 template <bool> struct CompileTimeChecker { CompileTimeChecker(...); //可变参数 }; template<> struct CompileTimeChecker<false> { }; #define STATIC_CHECK(expr, msg) \ { \ class ERROR_##msg{}; \ (void)sizeof(CompileTimeChecker<(expr)>(new ERROR_##msg())); \ } 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 main() { int i = 123; void* somePointer = &i; char c = safe_reinterpret_cast<char> (somePointer); system("pause"); }
编译错误:
error C2440: “”: 无法从“safe_reinterpret_cast::ERROR_Destination_Type_Too_Narrow *”转换为“CompileTimeChecker”
由此就可以通过自定义错误消息,来在编译期发现错误了!
2.2 Partial Template Specialization(模板偏特化)
模板偏特化 让你在 template的所有可能实体中特化出一组子集。
假如你创建以下类模板:
template <class Window, class Controller> class Widget { ... generic implementation ... };
然后加以特化
template <> class Widget<ModalDialog,MyController> { ... specialized implementation ... };
其中 ModalDialog和 MyController是你另外定义的classes。
有了这个Widget特化定义之后,如果你定义Widget<ModalDialog,MyController>对象,编译器就使用上述定义,如果你定义其他泛型对象,编译器就使用原本的泛型定义。
然而有时候你想要针对任意Window类并搭配一个特定的MyController类来特化Widget。
这时就需要模板偏特化机制:
//Partial Specialization of Widget template <class Window> //译注: Window仍是泛化 class Widget<Window,MyController> { //译注: MyController是特化 ... partially specialized implementation ... };
模板偏特化:泛化类 搭配 特定的普通类 来特化模板类的一种机制。
通常在一个class template偏特化定义中,你只会特化某些template参数而留下其他泛化参数。
当你在程序中具体实现上述class template时,编译器会试着找出最匹配的定义。这个寻找过程十分复杂精细,允许你以富创意的方式来进行偏特化。
例如,假设你有一个Button Classtemplate,它有一个template参数,
那么,你不但可以拿任意Window泛化参数搭配特定 MyController来特化Widget,还可以拿任意Button搭配特定MyController来偏特化Widget:
template <class ButtonArg> class Widget<Button<ButtonArg>, MyController> { ... further specialized implementation ... };
如你所见,偏特化的能力十分令人惊讶。当你具现化一个template时,编译器会把目前存在的偏特化和全特化 templates作比较,并找出其中最合适者。
优点:
- 这样的机制给了我们很大的弹性。
缺点:
- 不幸的是偏特化机制不能用在函数身上(不论成员函数或非成员函数〉,
- 这样多少会降低一些你所能做出来的弹性和粒度(granularity)。
需要注意的是:
- 虽然你可以全特化class template 中的成员函数,但你不能偏特化它们。
- 你不能偏特化namespace-level (non-member)函数。
最接近“namespace-level templatefunctions”偏特化机制的是函数重载─就实际运用而言,那意味着你对“函数参数”(而非返回值型别或内部所用型别)有很精致的特化能力。例如:
template <class T, class U> T Fun(U obj ) ;// primary template //template <class U> //void Fun<void,U>(U obj); // illegal partial //specialization template <class T> T Fun (Window obj ) ;//legal (overloading)
如果没有偏特化,编译器设计者的日子肯定会好过一些,但却对程序开发者造成不好的影响。稍后介绍的一些工具(如Int2Type和Type2Type)都显现偏特化的极限。本书频繁运用偏特化,typelist 的所有设施(第3章)几乎都建立在这个机制上。