读书笔记 effective c++ Item 27 尽量少使用转型(casting)
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)的更多相关文章
- 读书笔记 effective c++ Item 2 尽量使用const,枚举(enums),内联(inlines),不要使用宏定义(define)
这个条目叫做,尽量使用编译器而不要使用预处理器更好.#define并没有当作语言本身的一部分. 例如下面的例子: #define ASPECT_RATIO 1.653 符号名称永远不会被编译器看到.它 ...
- 读书笔记 effective c++ Item 26 尽量推迟变量的定义
1. 定义变量会引发构造和析构开销 每当你定义一种类型的变量时:当控制流到达变量的定义点时,你引入了调用构造函数的开销,当离开变量的作用域之后,你引入了调用析构函数的开销.对未使用到的变量同样会产生开 ...
- 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数
关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...
- 读书笔记 effective c++ Item 18 使接口容易被正确使用,不容易被误用
1. 什么样的接口才是好的接口 C++中充斥着接口:函数接口,类接口,模板接口.每个接口都是客户同你的代码进行交互的一种方法.假设你正在面对的是一些“讲道理”的人员,这些客户尝试把工作做好,他们希望能 ...
- 读书笔记 effective c++ Item 14 对资源管理类的拷贝行为要谨慎
1. 自己实现一个资源管理类 Item 13中介绍了 “资源获取之时也是初始化之时(RAII)”的概念,这个概念被当作资源管理类的“脊柱“,也描述了auto_ptr和tr1::shared_ptr是如 ...
- 读书笔记 effective c++ Item 16 成对使用new和delete时要用相同的形式
1. 一个错误释放内存的例子 下面的场景会有什么错? std::]; ... delete stringArray 一切看上去都是有序的.new匹配了一个delete.但有一些地方确实是错了.程序的行 ...
- 读书笔记 effective c++ Item 17 使用单独语句将new出来的对象放入智能指针
1. 可能会出现资源泄漏的一种用法 假设我们有一个获取进程优先权的函数,还有一个在动态分类的Widget对象上根据进程优先权进行一些操作的函数: int priority(); void proces ...
- 读书笔记 effective c++ Item 19 像设计类型(type)一样设计
1. 你需要重视类的设计 c++同其他面向对象编程语言一样,定义了一个新的类就相当于定义了一个新的类型(type),因此作为一个c++开发人员,大量时间会被花费在扩张你的类型系统上面.这意味着你不仅仅 ...
- 读书笔记 effective c++ Item 22 将数据成员声明成private
我们首先看一下为什么数据成员不应该是public的,然后我们将会看到应用在public数据成员上的论证同样适用于protected成员.最后够得出结论:数据成员应该是private的. 1. 为什么数 ...
随机推荐
- Java豆瓣电影爬虫——模拟登录的前世今生与验证码的爱恨情仇
前言 并不是所有的网站都能够敞开心扉让你看个透彻,它们总要给你出些难题让你觉得有些东西是来之不易的,往往,这也更加激发你的激情和斗志! 从<为了媳妇的一张号,我与百度医生杠上了>里就有网友 ...
- 1787: [Ahoi2008]Meet 紧急集合
1787: [Ahoi2008]Meet 紧急集合 Time Limit: 20 Sec Memory Limit: 162 MBSubmit: 1482 Solved: 652[Submit][ ...
- 3713: [PA2014]Iloczyn
3713: [PA2014]Iloczyn Time Limit: 1 Sec Memory Limit: 128 MBSubmit: 327 Solved: 181[Submit][Status ...
- 基于Spring-WS的Restful API的集成测试
在很多Java企业级应用中,Spring占据了非常重要的位置,这就导致了基本上的技术选型都是围绕着Spring来, 比方说笔者最近的项目需要开发一个Restful的API接口,选型的时候就说,客户架构 ...
- 学习ASP.NET MVC(十一)——分页
在这一篇文章中,我们将学习如何在MVC页面中实现分页的方法.分页功能是一个非常实用,常用的功能,当数据量过多的时候,必然要使用分页.在今天这篇文章中,我们学习如果在MVC页面中使用PagedList. ...
- 队列工厂之RedisMQ
本次和大家分享的是RedisMQ队列的用法,前两篇文章队列工厂之(MSMQ)和队列工厂之RabbitMQ分别简单介绍对应队列环境的搭建和常用方法的使用,加上本篇分享的RedisMQ那么就完成了咋们队列 ...
- windows phone 8.1开发:触控和指针事件1
原文出自:http://www.bcmeng.com/windows-phone-touch/ UIElement类的触控事件: ManipulationStarting:当用户将手指放在 IsMan ...
- 类string解析
原创作品,转载请注明来源:http://www.cnblogs.com/shrimp-can/p/5645248.html 在涉及字符串的时候,我们可以定义字符数组或指针,其实还有一个类,专门是为字符 ...
- mysql技术内幕InnoDB存储引擎-阅读笔记
mysql技术内幕InnoDB存储引擎这本书断断续续看了近10天左右,应该说作者有比较丰富的开发水平,在源码级别上分析的比较透彻.如果结合高可用mysql和高性能mysql来看或许效果会更好,可惜书太 ...
- Fireworks快捷键大全和ps查看切图的坐标颜色
记住后方便了许多