Effective C++——条款27(第5章)

本文探讨了C++转型的多种方式及其潜在风险,建议尽量减少使用转型,并特别注意dynamic_cast的性能影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

条款27:    尽量少做转型动作

Minimize casting

    C++规则的设计目标之一是,保证"类型错误"绝不可能发生.不幸的是,转型(cast)破坏了类型系统.

    首先回顾转型语法,因为通常有三种不同的形式,可写出相同的转型动作.C风格的转型动作看起来像这样:
(T)expression;                    // 将expression转型为T
    函数风格的转型动作看起来像这样:
T(expression);                    // 将expression转型为T
    两种形式并无差别,纯粹只是小括号的摆放位置不同而已.
    C++还提供了四种新式转型:
const_cast<T>(expression);
dynamic_cast<T>(expression);
reinterpret_cast<T>(expression);
static_cast<T>(expression);
    各有不同的目的(详见static_cast, dynamic_cast, const_cast探讨):
    const_cast
通常被用来 将对象的常量性转除(cast away the constness).它也是唯一有此能力的C++ style转型操作符.
     dynamic_cast 主要用来 执行"安全向下转型"(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型.它是唯一无法由就是旧式语法执行的动作,也是唯一 可能耗费重大运行成本的转型动作.
     reinterpret_cast 意图 执行低级转型,实际动作(及结果)可能取决于编译器,这就表示它 不可移植性.
     static_cast 用来 强迫隐式转换(implicit conversions),例如将non-const 对象转为 const 对象(就像 条款3(尽量使用const)所为),或将 int 转为 double 等等.它也可以用来执行上述多种转换中的反向转换,例如将 void* 指针转换为typed指针,将pointer-to-base转为pointer-to-derived.但它无法将 const 转为 non-const——这只有 const_cast 才办得到.
    旧式转换仍然合法,但 新式转换更受欢迎.原因是:第一,它们 很容易在代码中被辨识出来,因而得以简化"找出类型系统在哪个点被破坏"的过程.第二, 各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用.例如,如果打算将常量性(constness)去掉,除非使用新式转换中的 const_cast 否则无法通过编译.
    许多程序员认为, 转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型.这是错误的观念.任何一种类型转换(不论是通过转型操作而进行的显式转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的代码.例如:
int x, y;
double d = static_cast<double>(x)/y;
    将 int x 转型为 double 几乎肯定会产生一些代码,因为在大部分计算器体系结构中,int 的底层表述不同于 double 的底层表述.这或许不会令人惊讶,但请看下面这个例子:
class Base { ... };
class Derived : public Base { ... };
Derived d;
Base* pb = &d;                // 隐式地将Derived* 转换为Base*
    这里不过是建立一个base class 指针指向一个derived class 对象,但有时候上述的两个指针并不相同.这种情况下会有个偏移量(offset)在运行期间被施行于Derived* 指针上,用以取得正确的Base* 指针.
    上述例子表明,单一对象(例如一个类型为Derived的对象)可能拥有一个以上的地址(例如"以Base*指向它"时的地址和"以Derived*指向它"时的地址,实际上一旦使用多重继承,这种事情几乎一直发生,因此避免做出"对象在C++如何布局"的假设)行为.
    但请注意,只是有时候需要一个偏移量. 对象的布局方式和它们的地址计算方式随编译器的不同而不同,那意味着"由于知道对象如何布局"而设计的转型,不一定在所有平台都行得通.
    另一件关于转型的有趣事情是:很容易写出某些似是而非的代码.例如许多应用框架(application frameworks)都要求derived class 内的 virtual 函数代码的第一个动作就先调用base class 的对应函数.假设有个Window base class 和一个SpecialWindow derived class,两者都定义了 virtual 函数onResize.进一步假设SpecialWindow的OnResize函数被要求首先调用Window的OnResize.下面是实现方式之一,它看起来是对的,但实际是错的:
class Window {
public:
    ...
    virtual void OnResize() { ... }
};
class SpecialWindow : public Window {
    ...
    virtual void OnResize() {                    // derived onResize实现代码
        static_cast<Window>(*this).OnResize();    // 将*this转型为Window,然后调用其OnResize
        ...        // 这里进行SpecialWindow专属行为
    }
};
    这段程序将*this 转型为Window,对函数OnResize的调用也因此调用了Window::OnResize.但是,它调用的并不是当前对象上的函数,而是稍早转型动作建立的一个"*this对象的base class成分"的暂时副本身上的OnResize!再次强调上述代码并非在当期对象身上调用Window::OnResize之后又在该对象身上执行SpecialWindow专属动作.不,它是在"当前对象的base class成分"的副本上调用Window::OnResize,然后在当前对象身上执行SpecialWindow专属动作.如果Window::OnResize修改了对象内容,当前对象其实没有被改动,改动的是副本.
     解决之道是拿掉转型动作,替换为真正想实施的动作.并不像哄骗编译器将*this 视为一个base class 对象,只是想调用base class 版本的OnResize函数,令它作用于当前对象身上.所以请这样写:
class SpecialWindow : public Window {
public:
    virtual void OnResize() {
        Window::OnResize();        // 调用Window::OnResize作用于*this身上
        ...                        // SpecialWindow的专属动作
    }
};
    在探究 dynamic_cast 设计意涵之前,值得注意的是, dynamic_cast 的许多实现版本执行速度相当慢.例如至少有一个很普遍的实现版本基于"class名称的字符串比较",如果在四层深的单继承体系内的某个对象身上执行 dynamic_cast,则那个实现版本所提供的每一次dynamic_cast可能会耗用多达四次的strcpm调用,用以比较 class 名称.因此,除了对一般转型保持机敏与猜疑,更 应该在注重效率的代码中对dynamic_cast保持机敏与猜疑.
    之所以需要dynamic_cast,通常是因为想在一个认定为derived class 对象上执行derived class 操作函数,但手上却只有一个"指向base"的pointer或reference,只能靠它们来处理对象. 有两个一般性做法可以避免这个问题.
     第一,使用容器并在其中存储直接指向derived class 对象的指针(通常是智能指针,详见 条款13(以对象管理资源)),如此便消除了"通过base class接口处理对象"的需要.
    当然了,这种做法使得无法在同一个容器内存储指针"指向所有可能的各种派生类"。如果需要处理多种类型,可能需要多个容器,它们都必须具备类型安全性.
     另一种做法可让程序员通过base class 接口处理"所有的各种派生类",那就是在base class 内提供 virtual 函数做想做的对各种派生类做的事.
    不论哪一种写法——"使用类型安全容器"或"将virtual函数往继承体系上方移动"——都并非完全正确,但在许多情况下它们都提供一个可行的dynamic_cast替代方案.当它们有效时,就应该欣然拥抱它们.
    绝对必须避免的一件事就是所谓的"连串(cascading)dynamic_cast".
     优秀的C++代码很少使用转型,但若要完全摆脱它们又太过不切实际.因此,应该尽可能隔离转型动作,通常把它隐藏在某个函数内,函数接口会保护调用者不受函数内部任何动作的影响.
    注意:
    如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast.如果有个设计需要转型动作,试着发展无需转型的替代设计.
    如果转型是必要的,试着将它隐藏于某个函数背后.客户随后可以调用该函数,而不需将转型放进他们自己的代码内.
    宁可使用C++ style(新式)转型,不要使用旧式转型.前者很容易辨识出来,而且也比较有着分类的执掌.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值