《Effective C++ 中文版 第三版》读书笔记
** 条款 27:尽量少做转型动作 **
转型(casts)破坏了类型系统(type system)。可能导致任何关于类型的麻烦。
C++ 提供四种新式转型:
const_cast(expression) // cast away the constness
dynamic_cast(expression) // safe downcasting 安全向下转型
reinterpret_cast(expression) // 意图执行低级转型,实际动作(及结果)可能取决于编译器,这也表示它不可移植;低级代码以外很少见
static_cast(expression) // 用来强迫隐式转换(implicit conversions)
1.const_cast 通常被用来将对象的常量性转除(cast away the constness)。他也是唯一具有此能力的 C++ style 转型操作符。
2.dynamic_cast 主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属集成体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
3.reinterpret_cast 意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。
4.static_cast 用来强迫隐式转换,例如将 non-const 对象转型为 const 对象,或将 int 转型为 double 等等。他也可以用来执行上述多种转换的反向转换,例如将 void* 指针转换为 typed 指针,将 pointer-to-base 转为 pointer-to-derived。但它无法将 const 转为 non-const(这个只有 const_cast 能办到)。
C 风格旧式转型:1. (T)expression 2. T(expression)
新式转型比较受欢迎:1.很容易在代码中被辨别出来;2.各种转型动作愈窄化,编译器愈能诊断出错误的运用。
唯一使用旧式转型的时机是当调用一个 explicit 构造函数将一个对象传递给一个函数时:
class Widget{
public:
explicit Widget(int size);
...
};
void doSomeWork(const Widget &w);
doSomeWork(Widget(15)); // 函数风格的转型动作创建一个 Widget
doSomeWork(static_cast(15)); // C++ 风格的转型动作创建一个 Widget 对象
蓄意的 “对象生成” 动作不怎么像 “转型”,很可能使用函数风格的转型动作,而不使用 static_cast。但有时最好忽略你的感觉,始终理智的使用新式转型。
任何一个类型转换(不论是通过转型动作而进行的显示转换还是通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的码。例如在这段程序中:
int x, y;
...
double d = static_cast(x)/y; // 使用浮点数除法
将 int 转换成 double 几乎肯定会产生一些码,因为大部分计算机体系结构中,int 的底层表述不同于 double 的地层表述。下面这个例子可能让你稍微瞪大眼睛:
class Base{...};
class Derived : public Base{...};
Derived d;
Base* pb = &d; // 隐喻地将 derived* 转换成 Base*
这里我们只是建立一个 base class 指针指向一个 derived class 对象,但有时候上述的两个指针值并不相同。这种情况下会有个偏移量在运行期被施行于 Derived* 指针身上,用于取得正确的 Base* 指针值。
上述例子表明,单一对象(例如一个类型为 Derived 的对象)可能拥有一个以上的地址(例如“以 Base* 指向它”时的地址和以“ Derived* 指向它”时的地址)。C,java,C# 不可能发生这种事,但 C++ 可能!实际上一旦使用多重继承,这事几乎一直发生着。即使是在单一继承中也可能发生。虽然这还有其他意涵,但至少意味着你通常应该避免做出“对象在 C++ 中如何布局”的假设,更不应该以此假设为基础执行任何转型动作。例如将对象地址转型成 char* 指针,然后在他们身上进行指针运算,几乎总是导致无意义(不明确)行为。
对象的布局方式和他们的地址计算方式随编译器的不同而不同,那意味着“由于知道对象如何布局”而设计的转型,在某一平台行的通,在其他平台并不一定行得通。
另一件关于转型的有趣事情是:我们很容易写出某些似是而非的代码(其他语言中也许是对的)。例如 SpecialWindow 的 onResize 被要求首先调用 Window 的 onResize。下面是看起来对,实际上错:
class Window{
public:
virtual void onResize(){...}
...
};
class SpecialWinddow:public Window{
public:
virtual void onResize(){
static_cast(*this).onResize(); // 将*this转换成Window,然后调用其onResize;这样不行!
...
}
};
它调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个 “*this 对象的 base class 成分” 的暂时副本身上的 onResize!并不是在当前对象身上调用 Window::onResize 之后又在该对象上执行 SpecialWindow 专属行为。不,它是在 “当前对象之 base calss 成分” 的副本上调用 Window::onResize,然后在当前对象上执行 SpecialWindow 专属动作。如果 Window::onResize 修改了对象内容,当前对象其实没被改动,改动的是副本。然而 SpecialWindow::onResize 内如果也修改对象,当前对象真的会被改动。这使当前对象进入一种“伤残”状态:其 base class 成分的更改没有落实,而 derived class 成分的更改倒是落实了。
解决之道是拿掉转型动作,代之你真正想要说的话。所以,真正的解决方法是:
class SpecialWinddow:public Window{
public:
virtual void onResize(){
Window::onResize(); // 调用 Window::onResize 作用于 *this 身上
...
}
};
在探究 dynamic_cast 设计意涵之前,值得注意的是,dynamic_cast 的许多实现版本执行速度相当慢。假如至少有一个很普通的实现版本基于 “class 名称之字符串比较”,如果你在四层深的单继承体系内的某个对象身上执行 dynamic_cast,可能会耗用多达四次的 strcmp 调用,用以比较 class 名称。深度继承或多重继承的成本更高!某些实现版本这样做有其原因(它们必须支持动态链接)。在对注重效率的代码中更应该谨慎地使用 dynamic_cast。
之所以需要用 dynamic_cast,通常是因为我们想在一个我们认定为 derived class 对象身上执行 derived class 操作函数,但我们的手上只有一个“指向 base”的 pointer 或者 reference,你只能靠他们来处理对象。两个一般性做法可以避免这个问题:
一,使用容器,并在其中存储直接指向 derived class 对象的指针(通常是智能指针),如此便消除了 “通过 base class 接口处理对象” 的需要。假设先前的 Window/SpecialWindow 继承体系中有 SpecialWindows 才支持闪烁效果,试着不要这样做:
class Window{...};
class SpecialWindow:public Window{
public:
void blink();
};
typedef std::vector> VPW;
VPW winPtrs;
for (VPW::iterator iter = winPtrs.begin(); iter != vinPtrs.end(); ++iter)
{
if(SpecialWindow* psw = dynamic_cast(iter->get()))
psw->blink();
}
应该改而这样做:
typedef std::vector> VSPW;
VSPW winPtrs;
for (VSPW::iterator iter = winPtrs.begin(); iter != vinPtrs.end(); ++iter)
{
(*iter)->blink();
}
当然,这种做法无法在同一个容器内存储指针 “指向所有可能之各种 Window 派生类”。如果需要处理多种窗口类型,你可能需要多个容器,他们都必须具备类型安全性(type-safe)。
另一种做法让你通过 base class 接口处理 “所有可能之各种 window 派生类”,那就是在 base class 里提供 virtual 函数做你想对各个 Window 派生类做的事。虽然只有 SpecialWindows 可以闪烁,但或许将闪烁函数声明于 base class 内并提供一份什么也不做的缺省版本是有意义的:
class Window{
public:
virtual void blink(){}
};
class SpecialWindow:public Window{
public:
virtual void blink(){...};
};
typedef std::vector> VPW;
VPW winPtrs;
for (VPW::iterator iter = winPtrs.begin(); iter != vinPtrs.end(); ++iter)
{
(*iter)->blink();
}
无论哪一种写法,并非放之四海皆准,但在许多情况下它们都提供一个可行的 dynamic_cast 替代方案。当它们有此功效时,你应该欣然拥抱它们。
绝对必须拒绝的是所谓的 “连串(cascading)dynamic_casts”:
typedef std::vector> VPW;
VPW winPtrs;
for (VPW::iterator iter = winPtrs.begin(); iter != vinPtrs.end(); ++iter)
{
if (SpecialWindow1 * psw1 = dynamic_cast(iter->get())){...}
else if (SpecialWindow2 * psw1 = dynamic_cast(iter->get())){...}
else if (SpecialWindow3 * psw1 = dynamic_cast(iter->get())){...}
...
}
这样产生出来的代码又大又慢,而且基础不稳,因为每次 Window class 集成体系一有改变,所有这一类代码都必须再次检阅看看是否需要修改。例如一旦加入新的 derived class,或许上述连串判断中需要加入新的条件分支。这样的代码应该总是以某些 “基于 virtual 函数调用” 的东西取而代之。
优良的 C++ 代码很少使用转型,我们应该尽可能隔离转型动作,通常是把它隐藏在某个函数内,函数的接口会保护调用者不受函数内部任何肮脏龌龊的动作的影响。
请记住:
- 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。
- 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码内。
- 宁可使用 C++ style(新式)转型,不要使用旧式转型。前者很容易辨认出来,而且也有着比较分门别类的职责。