C++设计的规则是用来保证使类型相关的错误不再可能出现。理论上来说,如果你的程序能够很干净的通过编译,它就不会尝试在任何对象上执行任何不安全或无意义的操作。这个保证很有价值,不要轻易放弃它。

不幸的是,casts颠覆了类型系统。它导致了各种麻烦的出现,一些很容易识别,一些却很狡猾(不容易被识别)。如果你以前使用过C,java或者C#,你就需要注意了,因为在这些语言中casting是更加必不可少的,但却比C++更安全。C++不是C,不是java也不是C#,在C++中,你需要怀着极大的敬意来使用casting。

1. 新旧风格cast回顾

1.1 旧风格cast

先让我们回顾一下casting的语法,通常有三种不同的方法来实现同一个cast。C风格的casts如下:

 (T) expression // cast expression to be of type T

函数风格的casts使用下面的语法:

 T(expression) // cast expression to be of type T

上面的两种形式在意义上没有区别,只是放括号的地方不一样。我们将这两种形式的casts叫做旧式风格的casts。

1.2 C++风格的cast

C++同样提供四种新的casts形式(C++风格的casts):

 const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)

每种方法都有独特的用途:

  • Const_cast是用来去除对象的常量性的(constness)。在四个C++风格的cast中,const_cast是唯一能做到这一点的。
  • Dynamic_cast主要用来执行“安全的向下转型”,也就是决定一个特定类型的对象是否在一个继承体系中。这也是唯一一个不能用旧式风格语法来实现的cast它也是唯一一个可能会出现巨大的运行时开销的cast。(稍后会详细讲解)
  • Reinterpret_cast被用来做低级的casts,结果可能依赖于编译器,也就是代码不能被移植,例如,将一个指针转换成int。这种casts除了用在低级代码中,其他地方很少见。本书中只出现过一次,就是在讨论如何为原生内存(raw memory)实现一个调试分配器(Item 50)。
  • Static_cast能被用来做强制的显示类型转换(比如,non-const对象转换成const对象(Item 3),int转换成double等等。)它同样能够用来对这些转换进行反转(比如,void*转换成具体类型指针,指向base的指针转换成指向派生类的指针),但是不能从const转换成非const对象(只有const_cast能这么做)。

1.3 旧风格PK新风格

旧式风格的casts仍然合法,但是新风格的更好。第一,在代码中它们更加容易被辨别(对于人或者工具来说),因此简化了在代码中寻找转型动作的过程。第二,每个cast更加特别的使用用途使得编译器能够诊断出使用错误成为可能。譬如,如果你使用其它3个cast而不是const_cast来去除常量的常量性,你的代码无法通过编译。

我使用旧式风格cast的唯一地方是当我想通过调用一个explici构造函数来为一个函数传递一个对象的时候。比如:

 class Widget {
public:
explicit Widget(int size);
...
};
void doSomeWork(const Widget& w);
doSomeWork(Widget()); // create Widget from int
// with function-style cast doSomeWork(static_cast<Widget>()); // create Widget from int
// with C++-style cast

从某种意义上来说,这种对象的创建不像是一个cast,所以使用了函数风格的cast而不是static_cast。(这两种方法做了相同的事情:创建一个临时Widget对象然后传递给doSomeWork。)需要再说一遍,使用旧式转型实现的代码往往当时感觉很合理,但日后可能出现core dump,所以最好忽略这种感觉,总是使用新风格的casts。

2. 使用cast会产生运行时代码——不要认为你以为的就是你以为的

许多程序员认为cast除了告诉编译器需要将一个类型当作另外一个类型之外,没有做任何事情,但这个一个误区。任何种类的类型转换(不管显示cast还是隐式转换)都会产生运行时代码。举个例子:

 int x, y;
...
double d = static_cast<double>(x)/y; // divide x by y, but use
// floating point division

将int x转换成double肯定会产生代码,因为在大多数系统架构中,int的底层表示同double是不一样的。这也许不会让你吃惊,但下面的例子可能亮瞎你的双眼:

 class Base { ... };
class Derived: public Base { ... };
Derived d; Base *pb = &d; // implicitly convert Derived* ⇒ Base*

这里我们只是创建了一个指向派生类对象的基类指针,但有时候,这两个指针(Derived*和Base*)值将会不一样。在上面的情况中,运行时会在Derived*指针上应用一个偏移量来产生正确的Base*指针值。

最后这个例子表明一个对象(比如Derived类型的对象)可能有多于一个的地址(比如,当Base*指针指向这个对象和Derived*指向这个对象时有两个地址)。这在C,java和C#中不可能发生。事实上,当使用多继承时,这种情况总会发生,但在单继承中也能发生。这意味着在C++中你应该避免对一些东西是如何布局的做出假设。例如,将对象地址转换成char*指针然后在此指针上面进行指针算术运算几乎总是会产生未定义行为

但是注意我说过偏移量“有时候“是需要的。对象的布局方式和地址被计算的方式会随编译器的不同而不同。这意味着仅仅因为你了解一种平台上的布局和转型并不意味着在别的平台上也能如此工作。世界上充满了从中吸取教训的悲哀的程序员。

3. Cast很容易被误用——无效状态是如何产生的

关于cast的一件有趣的事情是容易写出看上去正确但实际错误的代码。比如,许多应用框架需要派生类中的虚函数实现首先要调用基类部分。假设我们有一个Window基类和一个SpecialWindow派生类,两个类中都定义了onResize虚函数。进一步假设SpecialWindow的onResize函数首先要调用Window的onResize函数。下面的实现方式看上去正确,实际上并非如此:

 class Window {                       // base class

 public:                                   

 virtual void onResize() { ... }           // base onResize impl

 ...                                                   

 };                                                   

 class SpecialWindow: public Window {       // derived class

 public:                                                       

 virtual void onResize() {                             // derived onResize impl;

 static_cast<Window>(*this).onResize();     // cast *this to Window,

 // then call its onResize;
// this doesn’t work!
... // do SpecialWindow-
} // specific stuff
... };

我已经对代码中的cast标注了红色。(它是新风格的cast,使用旧风格的转换也不会改变如下事实)。正如你所期望的,代码将*this转换成一个window对象。因此调用onResize时会触发Window:: onResize。你可能想不到的是它并没有在当前的对象上触发相应的函数。相反,转型动作为*this的基类部分创建了一份新的临时拷贝,onResize是在这份拷贝上被触发的!上面的代码没有在当前对象上调用Window::onResize然后在此对象上执行SpecialWindow的指定动作——它在执行特定动作之前,在当前对象基类部分的拷贝之上调用了Window::onResize。如果Window::onResize修改了当前对象(很有可能,既然onResize是non-const成员函数),当前的对象(Window对象)是不会被修改的。修改的是对象的拷贝。然而如果SpecialWIndow::onResize修改当前对象,当前对象将会被修改,导致上面代码会为当前对象留下一个无效状态:基类部分没有被修改,派生类部分却被修改了

解决方法是消除cast的使用,你不想欺骗编译器让其把*this当作一个基类对象。你想的是在当前对象上调用onResize的基类版本。所以按照下面的方法做:

 class SpecialWindow: public Window {
public:
virtual void onResize() {
Window::onResize(); // call Window::onResize
... // on *this
}
...
};

这个例子同样表明如果你发现你自己想使用cast了,它就标志着你可能会使用错误的方式来应用它。使用dynamic_cast的时候也是如此。

4. Dynamic_cast 分析

4.1 Dynamic_cast速度很慢

在深入研究dynamic_cast的设计含义之前,我们能观察到dynamic_cast的很多实现其速度是非常慢的。举个例子,至少有一种普通的实现在某种程度上是基于类名称的字符串比较。如果你正在一个4层深的单继承体系的对象上执行dynamic_cast,在这样一种实现(也就是上面说的普通实现)下每个dynamic_cast至多可能调用四次strcmp来比较类名称。一个层次更深的继承或者一个多继承可能开销会更大。这样实现是有原因的(它们必须支持动态链接(dynamic linking))。因此,除了要对使用cast时的一般问题保持机敏,在对性能敏感的代码中更要对dynamic_cast的使用保持机敏

4.2 Dynamic_cast的两种替代方案

你需要dynamic_cast是因为你想在你坚信其是派生类对象之上执行派生类操作,但你只能通过基类指针或基类引用来操作此对象。有两种普通的方法避免使用dynamic_cast

第一,  使用容器直接存储派生类对象指针(通常情况下使用智能指针,见Item 13),这样就消除了通过基类接口来操纵这些对象的可能。举个例子,在我们的window/SpecialWindow继承体系中,只有SpecialWindows支持blink,不要像下面这样做:

 class Window { ... };
class SpecialWindow: public Window {
public:
void blink();
...
}; typedef // see Item 13 for info std::vector<std::tr1::shared_ptr<Window> > VPW; // on tr1::shared_ptr VPW winPtrs; ... for (VPW::iterator iter = winPtrs.begin(); // undesirable code: iter != winPtrs.end(); // uses dynamic_cast ++iter) { if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get())) psw->blink(); }

而是用下面的做法:

 typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW;

 VPSW winPtrs;

 ...

 for (VPSW::iterator iter = winPtrs.begin(); // better code: uses
iter != winPtrs.end(); // no dynamic_cast
++iter)
(*iter)->blink();

当然这种方法不允许你在同一个容器中存储所有可能的Window派生物。要达到这个目的,你可能需要多个类型安全的容器。

第二,  在基类中提供虚函数。举个例子,虽然只有SecialWindos支持blink,你同样可以在基类中声明一个blink,但默认实现是什么都不做:

 class Window {
public:
virtual void blink() {} // default impl is no-op;
... // see Item 34 for why
}; // a default impl may be
// a bad idea
class SpecialWindow: public Window {
public:
virtual void blink() { ... } // in this class, blink ... // does something }; typedef std::vector<std::tr1::shared_ptr<Window> > VPW; VPW winPtrs; // container holds
// (ptrs to) all possible
... // Window types
for (VPW::iterator iter = winPtrs.begin();
iter != winPtrs.end();
++iter) // note lack of
(*iter)->blink(); // dynamic_cast

上面的两种方法不是在任何情况下都能使用,但是在许多情况下,它们为dynamic_cast提供了一种可行的替代方案。当他们确实能做到你想要的,你应该拥抱它们。

4.3 不要在级联设计中使用dynamic_cast

你绝对想避免的一件事是不要做包含级联dynamic_cast的设计,也就是像下面这个样子:

 class Window { ... };

 ...

 // derived classes are defined here

 typedef std::vector<std::tr1::shared_ptr<Window> > VPW;

 VPW winPtrs;

 ...

 for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)

 {

 if (SpecialWindow1 *psw1 =

 dynamic_cast<SpecialWindow1*>(iter->get())) { ... }

 else if (SpecialWindow2 *psw2 =

 dynamic_cast<SpecialWindow2*>(iter->get())) { ... }

 else if (SpecialWindow3 *psw3 =

 dynamic_cast<SpecialWindow3*>(iter->get())) { ... }

 ...

 }

这种实现产生的代码既大又慢,也很脆弱,因为每次Windos类体系发生变化,你都需要为上面的代码做一次检查是否需要更新。(例如,如果添加了一个新的派生类,上面的代码可能需要添加一个新的条件分支)。这样的代码应该被基于虚函数的设计替换掉。

5. 把对cast的使用隐藏在函数接口中

好的C++ 代码很少使用casts,但完全去除它们也是不切实际的。Int 转换成double这样的cast是合理的应用,虽然有可能不是必须的。(可以重新声明一个新的double变量,用x的值来对其进行初始化)。像许多可能令人起疑的设计一样,要尽可能的对cast的使用进行隔离,可以将其隐藏在调用者看不见的接口中。

6. 总结:

    • 能避免就避免使用cast,尤其在对性能敏感的代码中对使用dynamic_cast要谨慎。如果一个设计需要cast,首先尝试是否能设计出一个不需要cast的替代方案。
    • 当必须使用casting的时候,尽量将其隐藏在函数中。客户可以调用这个函数从而避免在他们自己的代码中使用casts
    • 优先使用C++风格的cast而不是旧式风格的casts。因为它们很容易被看到,它们做的事情也更加明确。

读书笔记 effective c++ Item 27 尽量少使用转型(casting)的更多相关文章

  1. 读书笔记 effective c++ Item 2 尽量使用const,枚举(enums),内联(inlines),不要使用宏定义(define)

    这个条目叫做,尽量使用编译器而不要使用预处理器更好.#define并没有当作语言本身的一部分. 例如下面的例子: #define ASPECT_RATIO 1.653 符号名称永远不会被编译器看到.它 ...

  2. 读书笔记 effective c++ Item 26 尽量推迟变量的定义

    1. 定义变量会引发构造和析构开销 每当你定义一种类型的变量时:当控制流到达变量的定义点时,你引入了调用构造函数的开销,当离开变量的作用域之后,你引入了调用析构函数的开销.对未使用到的变量同样会产生开 ...

  3. 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数

    关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...

  4. 读书笔记 effective c++ Item 18 使接口容易被正确使用,不容易被误用

    1. 什么样的接口才是好的接口 C++中充斥着接口:函数接口,类接口,模板接口.每个接口都是客户同你的代码进行交互的一种方法.假设你正在面对的是一些“讲道理”的人员,这些客户尝试把工作做好,他们希望能 ...

  5. 读书笔记 effective c++ Item 14 对资源管理类的拷贝行为要谨慎

    1. 自己实现一个资源管理类 Item 13中介绍了 “资源获取之时也是初始化之时(RAII)”的概念,这个概念被当作资源管理类的“脊柱“,也描述了auto_ptr和tr1::shared_ptr是如 ...

  6. 读书笔记 effective c++ Item 16 成对使用new和delete时要用相同的形式

    1. 一个错误释放内存的例子 下面的场景会有什么错? std::]; ... delete stringArray 一切看上去都是有序的.new匹配了一个delete.但有一些地方确实是错了.程序的行 ...

  7. 读书笔记 effective c++ Item 17 使用单独语句将new出来的对象放入智能指针

    1. 可能会出现资源泄漏的一种用法 假设我们有一个获取进程优先权的函数,还有一个在动态分类的Widget对象上根据进程优先权进行一些操作的函数: int priority(); void proces ...

  8. 读书笔记 effective c++ Item 19 像设计类型(type)一样设计

    1. 你需要重视类的设计 c++同其他面向对象编程语言一样,定义了一个新的类就相当于定义了一个新的类型(type),因此作为一个c++开发人员,大量时间会被花费在扩张你的类型系统上面.这意味着你不仅仅 ...

  9. 读书笔记 effective c++ Item 22 将数据成员声明成private

    我们首先看一下为什么数据成员不应该是public的,然后我们将会看到应用在public数据成员上的论证同样适用于protected成员.最后够得出结论:数据成员应该是private的. 1. 为什么数 ...

随机推荐

  1. ACM Fibonacci数 计算

    Fibonacci数 时间限制:3000 ms  |  内存限制:65535 KB 难度:1   描述 无穷数列1,1,2,3,5,8,13,21,34,55...称为Fibonacci数列,它可以递 ...

  2. KoaHub.js -- 基于 Koa.js 平台的 Node.js web 快速开发框架之koahub-body-res

    koahub body res Format koa's respond json. Installation $ npm install koahub-body-res Use with koa v ...

  3. Rabbitmq 性能测试

    背景: 线上环境,出了一起事故,初步定位是rabbitmq server. 通过抓包发现,是有多个应用使用同一台rabbitmq server.并且多个应用使用rabbitmq的方式也不一样.发现有以 ...

  4. Ubuntu 16.04安装DB2 Express C v11.1

    欢迎和大家交流技术相关问题:邮箱: jiangxinnju@163.com博客园地址: http://www.cnblogs.com/jiangxinnjuGitHub地址: https://gith ...

  5. Java标准注释配置

    eclipse中java文件头注释格式设置 windows->preferences->java->Code Templates->comments->Type-> ...

  6. C#传递委托给C或C++库报错__对XXX类型的已垃圾回收委托进行了回调

    出现的原因: 因为你传给C或C++的委托是局部的.可能传过去之后就被垃圾回收了,再次调用就会异常. 想办法做成全局的就好 public void Play(string url) { _bassStr ...

  7. 构建高性能web站点-阅读笔记(一)

    看完前9章,也算是看完一半了吧,总结一下. 郭欣这个名字或许并不响亮,但是这本书写的确实真好!百度一下他的名字也能够看到他是某些公司的创始人和投资者,当然他本人必定是大牛无疑. 从网页的动静分离到网络 ...

  8. mysq常用l性能分析方法

    orzdba查看读写./orzdba.pl --mysql -S /data/mysql30001/mysql.sock 语句查看读写命令数量,以及数据库TPS,传输的大小 查看processlist ...

  9. 【C++】指针与引用的区别

    本文主要总结在C++中指针与引用的区别. 从定义与性质来看指针与引用有如下区别: 指针表示的是一块变量的地址 引用表示一个变量的别名. 因此指针变量需要占用空间(一个指针变量在32位系统下占用4字节, ...

  10. 什么是node.js

    1.0什么是nodejs 1.1定义: Node.js是Javascript除了浏览器之外可以运行的另一个环境(runtime).可以为我们提供开启服务功能和提供文件读写功能 1.2特点: 1)基于g ...