强制转换表示法和 safe_cast<> 介绍
更新:2007 年 11 月
从 C++ 托管扩展到 Visual C++ 2008,强制转换表示法已发生更改。
修改现有结构与从头创建初始结构不同,而且要困难得多。修改现有结构时,编程的自由度受到限制,不得不在理想的重构方案和现有特定结构依赖关系之间进行折衷选择。
语言扩展是另外一个例子。回溯到二十世纪九十年代早期,面向对象的编程在当时成为一种重要的范例,在 C++ 中建立类型安全的向下转换功能变得非常迫切。向下转换是从基类指针或引用到派生类的指针或引用的由用户进行的显式转换。向下转换要求进行显式的转换。这是因为基类指针的实际类型是运行库的一个方面;因此,编译器无法对它进行检查。或者换句话说,向下转换功能就像虚函数调用一样,要求某种形式的动态解析。这会引发两个问题:
为什么向下转换在面向对象的范例中是必要的? 虚函数机制的不能满足需要? 也就是说,难道不能说之所以需要向下转换(或任何种类的强制转换)都是由于设计失败造成的吗?
为什么对向下转换的支持会是 C++ 中的一个问题? 毕竟,它对于 Smalltalk 等面向对象的语言(或后来的 Java 和 C#)来说不是问题。对于 C++ 来说,是什么使得支持向下转换功能如此困难?
虚函数代表类型系列中常见的依赖于类型的算法。(我们不考虑接口,ISO-C++ 中不支持接口,但在 CLR 编程中可以使用接口,接口代表一种有趣的设计替代方法)。该系列的设计通常由一个类层次结构表示,在类层次结构中,存在一个声明公共接口(虚函数)的抽象基类,以及一组表示应用程序域中的实际类型系列的具体派生类。
例如,Computer Generated Imagery (CGI) 应用程序域中的 Light 层次结构将具有公共的属性,如 color、intensity、position、on、off 等。人们使用公共接口就可以控制多种光源,而不必担心特定光源是点光源、定向光源、非定向光源(如太阳)还是遮光板光源。在这种情况下,则无需向下转换为特定光源类型来使用其虚拟接口。但在生产环境中,速度是至关重要的。人们这样做可以向下转换和显式调用每个方法,以便能够在不使用虚拟机制的情况下内联执行调用。
因此,在 C++ 中进行向下转换的一个原因是为了避免虚拟机制,从而显著提高运行时性能。(请注意,这种手动优化的自动化是当前的一个研究热点。但是,解决此问题比替换 register 或 inline 关键字的显式使用更为困难。)
向下转换的另一个原因来自多态性的双重性质。多态性可以看作能够划分为一对形式:被动形式和动态形式。
虚拟调用(以及向下转换功能)表示多态性的动态使用:在程序执行过程中,根据特定实例处的基类指针的实际类型执行操作。
然而,将派生类对象赋给其基类指针是多态性的被动形式;这里是将多态性用作了一种传输机制。例如,在引入泛型之前的 CLR 编程中,这是 Object 的主要用途。在以被动形式使用时,为传输和存储选择的基类指针通常提供过于抽象的接口。例如,Object 通过其接口提供大约五种方法;任何更为具体的行为都需要进行显式的向下转换。例如,如果要调整点光源的角度或衰减速率,就必须显式地向下转换。子类型系列内的虚拟接口实际上不可能成为它的众多子级的所有可能的方法的超集,因此在面向对象的语言内,始终需要一种向下转换功能。
如果面向对象的语言中需要安全的向下转换功能,那么为什么在 C++ 中添加这一功能用了这么长时间? 问题在于如何使关于指针的运行时类型的信息可用。对于虚函数,编译器将运行时信息分为两部分进行组织:
类对象中包含一个附加的虚拟表指针成员(位于类对象的开始或结尾部分,本身的历史颇为有趣),该成员指向相应的虚拟表。例如,点光源对象指向点光源虚拟表,定向光源指向定向光源虚拟表等等。
每个虚函数都在该表中有一个关联的固定位置,实际要调用的实例由存储在该表中的地址表示。例如,虚拟 Light 析构函数可能与位置 0 相关联,Color 与位置 1 相关联,依此类推。这是高效但不够灵活的策略,因为它是在编译时设置的,但系统开销较小。
接下来的问题就是如何在不更改 C++ 指针大小的情况下使类型信息对指针可用:要么通过添加另一个地址,要么直接添加某种类型编码。对于决定不使用面向对象范例的程序员(以及程序)来说,这是不可接受的,而这些程序员仍是用户中的大多数。另一种可能性是为多态类类型引入一个特殊指针,但是这种方法可能非常令人费解,并且使混合两者变得非常困难,对指针算法问题来说尤其如此。维护一个将各个指针与其当前关联的类型关联的运行时表,并动态更新该表,也是不可接受的。
现在问题就是:有两个用户群体,他们有不同但都合理的编程要求。解决方案必须是这两个群体之间的一项妥协,使得每个群体能够在保持自己的要求的情况下具备交互操作的能力。这意味着由任何一方提出的解决方案都可能是不可行的,而最终实现的解决方案也将不那么完美。实际的解决办法关键在于多态类的定义:多态类是包含虚函数的类。多态类支持动态的类型安全的向下转换。这就解决了“维护作为地址的指针”的问题,因为所有多态类都包含指向其关联虚表的附加指针成员。因此,关联的类型信息可存储在扩展的虚表结构中。类型安全的向下转换的开销基本上都由该功能的用户负担。
与类型安全的向下转换有关的下一个问题是它的语法。由于它是强制转换,提交给 ISO-C++ 委员会的最初建议稿使用了未加修饰的强制转换语法,如下面的示例中所示:
spot = ( SpotLight* ) plight;
但是委员会拒绝了此建议,因为它不允许用户控制强制转换的开销。如果动态的类型安全向下转换与以前不安全且静态的强制转换表示法具有相同的语法,它就成了一种替代物,用户将无法在向下转换并无必要或者开销太大的情况下取消运行时系统开销。
通常,C++ 中始终有可用来取消编译器支持的功能的机制。例如,我们可使用类范围运算符 (Box::rotate(angle)),或通过类对象(而不是该类的指针或引用)调用虚方法,来关闭虚拟机制。后一种取消方法不是语言要求,而是实现质量问题,类似于在窗体声明中不显示临时构造:
// compilers are free to optimize away the temporary
X x = X::X( 10 );
因此,该建议被退回以进行进一步考虑,实际上也考虑过几种替代表示法,而返回给委员会的表示法采用了 (?type) 的形式,表示它的不确定性(即动态性)。这使得用户能够在两种形式(静态或动态)之间切换,但是没有谁对此非常热心。因此,只好再从头来过。第三种表示法,也就是成功的表示法是现在的标准 dynamic_cast<type>,它被推广为一组新式的强制转换表示法(共有四个)。
在 ISO-C++ 中,dynamic_cast 在应用于不适当的指针类型时返回 0,在应用于引用类型时引发 std::bad_cast 异常。在 C++ 托管扩展中,将 dynamic_cast 应用于托管引用类型(由于其指针表示形式)始终返回 0。引入了 __try_cast<type>,作为对 dynamic_cast 异常引发变体的模拟,不同之处是在强制转换失败时,它将引发 System::InvalidCastException。
public __gc class ItemVerb;
public __gc class ItemVerbCollection {
public:
ItemVerb *EnsureVerbArray() [] {
return __try_cast<ItemVerb *[]>
(verbList->ToArray(__typeof(ItemVerb *)));
}
};
在新语法中,__try_cast 已被重新强制转换为 safe_cast。使用新语法的相同代码片段如下:
public ref class ItemVerb;
public ref class ItemVerbCollection {
public:
array<ItemVerb^>^ EnsureVerbArray() {
return safe_cast<array<ItemVerb^>^>
( verbList->ToArray( ItemVerb::typeid ));
}
};
在托管领域中,很重要的一点是:应当限制程序员在类型之间以使代码不可验证的方式执行强制转换的能力,以便实现可验证的代码。这是新语法所代表的动态编程范例的一个重要方面。因此,旧式强制转换的实例被在内部重新强制转换为运行时强制转换,例如:
// internally recast into the
// equivalent safe_cast expression above
( array<ItemVerb^>^ ) verbList->ToArray( ItemVerb::typeid );
另一方面,由于多态性同时提供主动模式和被动模式,有时需要单单为了访问子类型的非虚拟 API 而执行向下转换。例如,当类的成员要对层次结构内任何类型进行寻址(将被动多态性作为传输机制),而特定程序上下文内该成员的实际实例已知时,可能会发生上述情况。在这种情况下,对强制转换进行运行时检查开销很高,令人难以接受。如果新语法要作为托管系统编程语言,它必须提供几种可进行编译时(也就是静态)向下转换的方法。这就是允许 static_cast 表示法的应用作为编译时向下转换被保留的原因:
// ok: cast performed at compile-time.
// No run-time check for type correctness
static_cast< array<ItemVerb^>^>(verbList->ToArray(ItemVerb::typeid));
问题是无法保证执行 static_cast 的程序员是正确和善意的;也就是说,无法强制托管代码是可验证的。与本机范例相比,在动态程序范例之下,这是个更加急需关注的问题,但是,在系统编程语言内不允许用户在静态和运行时强制转换之间切换是不够的。
但新语法中存在性能陷阱和缺陷。在本机编程中,旧式强制转换表示法和新式的 static_cast 表示法在性能上没有差异。但在新语法中,旧式强制转换表示法比新式 static_cast 表示法的开销要高很多。原因是编译器会在内部将使用的旧式表示法转换为可引发异常的运行时检查。此外,它还改变了代码的执行流程,因为它会产生未捕获的异常并导致应用程序中断 — 或许这样做是明智的,但是,如果使用 static_cast 表示法,同样的错误不会导致该异常。有人可能会说这将促使用户使用新式的表示法。但是这种情况仅在旧式表示法失败时才会发生;否则,它会导致使用旧式表示法的程序在没有明显原因的情况下显著降低运行速度,类似于下面的 C 程序员缺陷:
// pitfall # 1:
// initialization can remove a temporary class object,
// assignment cannot
Matrix m;
m = another_matrix;
// pitfall # 2: declaration of class objects far from their use
Matrix m( 2000, 2000 ), n( 2000, 2000 );
if ( ! mumble ) return;