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

c++规则的设计目标之一是,保证“类型错误”绝不可能发生。
理论上如果您的程序很“干净地”通过编译,就表示它并不企图在任何对象身上执行任何不安全、无意义、愚蠢荒谬的操作。
不幸的是,转型破坏了类型系统。那可能导致任何种类的麻烦,有些容易辨识,有些非常隐晦。

1、转型语法

首先回顾转型语法,通常有三种不同的形式,可写出相同的转型动作:
C风格的转型动作看起来像这样:

(T)expression //将expression转型为T

函数风格的转型动作看起来像这样:

T(expression) //将expression转型为T

两种形式并无差别,纯粹只是小括号的摆放位置不同而已。这两种形式称为“旧式转型”。
C++还提供四种新式转型(常常被称为new-style或c++-style casts):

const_cast<T>( expression ) //将对象的常量性移除,唯一有此能力的c++-style转型操作符
dynamic_cast<T>( expression ) //主要用来执行“安全向下转型”
reinterpret_cast<T>( expression ) //意图执行低级转型,实际动作可能取决于编译器(可以在任何类型之间转换)
static_cast<T>( expression ) //用来强迫隐式转换,也可以将基类指针转换为派生类指针

旧式转型依然合法,新式转型较受欢迎。因为:
第一,它们很容易在代码中被辨识出来,因而得以简化找出类型系统在哪个地点被破坏的过程;
第二,各转型动作的目标越窄化,编译器越可能诊断出错误的运用。例如,如果打算将常量性去除,除非使用新型的const_cast否则无法通过编译。

2、任何一个类型转换往往真的领编译器变溢出运行期间执行的码

class Base {...};
class Derived: public Base {...};
Derived d;
Base* pb = &d;

这里我们建立了一个base class指针指向一个derived class对象,但有时候上述的两个指针的值并不相同。这种情况下会有个偏移量在运行期被施行于Derived* 指针身上,用以缺德正确的Base* 指针值。
上述例子表面,单一对象可能拥有一个以上的地址(例如分别以Base*指针和Derived*指针指向它时的地址)
实际上一旦使用多重继承,这事几乎一直发生着。即使在单一继承中也可能发生。这意味着你通常应该避免做出“对象在c++中如何布局”的假设,更不应该以此假设为基础执行任何转型动作。例如,将对象地址转型为char*指针然后在它们身上执行指针算术,几乎总是会导致无定义行为。
对象的布局方式和它们的地址计算方式随编译器的不同而不同,那意味着“由于知道对象如何布局”而设计的转型,在某一平台行得通,在其他平台并不一定行得通。

3、当derived class内的virtual函数的代码需要调用base class内对应函数时

class Window {
    public:
        virtual void onResize() {...}
        ...
};
class SpecialWindow : public Window {
    public:
        virtual void onResize() {
            static_cast<Window>(*this).onResize(); // 将*this转型为Window,
                                                   // 然后调用其onResize;
                                                   // 这不可行!
            ...
        }
};

你恐怕没有想到,它调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个“*this对象的base class成分”的暂时副本身上的onResize()。也即,上述代码并非在当前对象身上调用Window::onResize之后又在该对象身上执行SpecialWindow专属动作,它是在base成分的副本上调用Window::onResize,然后在当前对象身上执行SpecialWindow专属动作,如果Window::onResize()修改了对象内容,当前对象其实没被改动,改动的是副本!然而SpecialWindow::onResize内如果也修改对象,当前对象真的会被改动,这就导致了不好的结果。
解决之道是拿掉转型动作,代之以你真正想说的话。

class SpecialWindow : public Window {
    public:
        virtual void onResize() {
            Window::onResize();// 调用Window::onResize作用于*this身上
            ...
        }
};

4、dynamic_cast的许多实现版本执行速度相当慢

例如至少有一个很普遍的实现版本基于“class名称的字符串比较”,如果你在四层深的单继承体系内的某个对象身上执行dynamic_cast,刚才说的那个实现版本所提供的每一次dynamic_cast可能会耗用多达四次的strcmp调用,用以比较class名称。深度继承或多重继承的成本更高!某些实现版本这样做有其原因,但是还是要强调,除了对一般转型保持机敏与猜疑,更应该在注重效率的代码中对dynamic_casts保持机敏与猜疑。
之所以需要dynamic_cast,通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但你的手上只有一个指向base的指针或引用,你只能靠他们来处理对象。有两个一般性做法可以避免这个问题。
第一,使用容器并在其中存储直接指向derived class对象的指针(通常是智能指针),如此便消除了“通过base class接口处理对象”的需要。例如,用vector存储Derive class的智能指针。但是这种做法使你无法在同一容器内存储指针“指向所有可能的各种Base的派生类”,如果要处理多种派生类,你可能需要多个容器,他们都必须具备类型安全性。
另一种做法可以让你通过base class接口处理“所有可能的各种Base派生类”,那就是在base class内提供virtual函数做你想对各个派生类做的事情。而基类中该函数缺省实现代码中应该什么也不做。
绝对必须避免的一件事是所谓的“连串dynamic_casts”,这样产生出来的代码又大又慢,而且基础不稳,因为每次Base class继承体系一有改变,所有这一类代码都必须再次检阅看看是否需要修改。

4、如果转型是必须的,试着将它隐藏于某个函数背后

优良的C++代码很少使用转型,但若说要完全摆脱它们又太过不切实际。我们应该尽可能隔离转型动作,通常是把它隐藏在某个函数内,函数的接口会保护调用者不受函数内部任何肮脏龌龊的动作影响。

猜你喜欢

转载自blog.csdn.net/unirrrrr/article/details/81328324