《Effective C++》学习笔记——条款27

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

五、Implementations

Rule 27:Minimize casting

规则 27:尽量少做转型动作

1.一些基础

C++规则的设计目标之一 —— 保证“类型错误”绝对不可能发生。

理论上,如果程序很"干净地"通过编译,就表示它并不企图在任何对象身上执行任何不安全、无意义、愚蠢荒谬的操作。

But,转型(cast)破坏了类型系统(type system),这种可能会导致任何种类的麻烦,而且这些麻烦繁琐度不一。

如果你用的是C、Java 或 C# ,这点就需要特别注意一下,因为那些语言中的转型比较必要而无法避免,与C++相对比较而言,也不是特别危险。

关于转型(cast),这里通常有三种不同的形式:( 都是将expression转型为T )

—— C风格的转型动作:  (T)expression

—— 函数风格的转型动作:  T(expression)

这两种形式没有差别,只是小括号的位置不同而已,可以称这两种形式为 " 旧式转型 "

C++ 还提供了四种新式转型(常常被称为 new-style 或者 C++-style )

—— const_cast<T> ( expression )

? 这种通常用来将对象的常量性转除。它也是 唯一 有此能力的C++-style转型操作符。

—— dynamic_cast<T> ( expression )

? 主要用来执行 ”安全向下转型 “,也就是用来决定某对象是否归属继承体系中的某个类型。 它是 唯一 无法由旧式语法执行的动作,也是 唯一 可能耗费重大运行成本的转型动作。

—— reinterpret_cast<T> ( expression )

? 执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。 例如:将一个 pointer to int 转型为 int。这一类转型在低级代码以外很少见。

—— static_cast<T> ( expression )

? 用来执行强迫隐式转换,例如:将一个 non-const 对象转换为 const 对象,或将一个 int 转为 double等等。它也可以用来执行上述多种转换的反向转换,例如:将 void* 指针转为 typed 指针,将 pointer-to-base 转为 pointer-t0-derived。但它无法将 const 转为 non-const,这个只有 const_cast才办得到。

这么多种形式的转型,虽然旧式转型仍然合法,但新式转型较受欢迎。原因有二:

> 它们很容易在代码中被辨识出来(不论是人工辨识或使用工具),因而得以简化“找出类型系统在哪个地点被破坏”的过程。

> 各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。

PS: 有一个 唯一  使用旧式转型的时机,当需要调用一个 explicit 构造函数将一个对象传递给一个函数时。

比如:

class Widget  {
public:
    explicit Widget( int size );
    ...
};
void doSomeWork( const Widget& w );
// 以一个int加上“函数风格”的转型动作创建一个Widget
doSomeWork( Widget(15) );
// 以一个int加上“C++风格”的转型动作创建一个Widget
doSomeWork( static_cast<Widget>(15) );

2.一些东西

① 许多程序员认为,转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型。But,这是错误的观念。

任何一个类型转换往往真的令编译器编译出运行期间执行的码。

例如在下面这段程序中:

int x,y;
...
double d = static_cast<double>(x)/y;    // x除以y,使用浮点数除法

将int转型为double几乎肯定会产生一些代码,因为在大部分计算器体系结构中,int的底层表述不同于double的底层表述。

但在下面这个例子,尤其需要注意一下:

class Base  { ... };
class Derived : public Base  { ... };
Derived d;
Base* pb = &d;    // 隐喻的将Derived* 转换为 Base* 

在这里,不过是建立一个 基类的指针 指向一个 派生类的对象,但有时候上述两个指针值并不相同。这种情况下会有一个 偏移量 在运行期被施行于 派生类指针身上,用以取得正确的 基类指针值。

上面这个例子已经表明,单一对象(例如一个类型为Derived的对象)可能拥有一个以上的地址(比如 以"Base* 指向 " 和 以" Derived* 指向" 时的地址),在C、Java、C#中都不可能发生这种事,唯独C++中可以,实际上一旦使用多重继承,它就一直发生,即使是单一继承中也可能发生。虽然这里面可能有些其他的东西,但至少意味着我们应该避免做出"对象在C++中如何如何布局"的假设,更不该以此假设为基础执行任何转型动作。

② 我们很容易写出某些似是而非的代码(即使是在别的语言中)。

比如,许多应用框架都要求 派生类内的 虚函数 代码的第一个动作就先调用基类的对应函数。假设我们有个 Window base class 和一个 SpecialWindow derived class,两者都定义了 virtual函数 onResize。进一步假设
SpecialWindow 的 onResize 函数被要求收先调用Window的onResize。下面是一种实现方法(似是而非的)

class Window  {    // 基类
public:
  virtual void onResize()  { ... }
  ...
};

class SpecialWindow : public Window  {    // 派生类
public:
  virtual void onResize()  {
    static_cast<Window>(*this).onResize();    // 将*this转型为Window,然后调用其onResize  ,这样是错的
    ...
  }
  ...
};

稍微解释一下,在此代码中强调了转型动作(此处用了新式转型)。这段程序将*this转型为Window,对函数onResize的调用也因此调用了Window::onResize。但它调用的并不是当前对象的函数,而是稍早转型动作所建立的一个"*this对象的基类成分"的暂时副本上的onResize。

? 这个问题的解决方法就是——拿掉转型动作,直说。

比如,如果只是想调用 基类 版本的onResize函数,令它作用于当前对象身上。所以这么写:

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

在这个例子中,可以发现:如果想用转型这个动作,这就等于一个warning,因为可能正将局面发展至错误的方向上,尤其是当使用dynamic_cast的时候。

③ 对于 dynamic_cast 有些需要注意的地方,它的许多实现版本执行速度相当慢。例如至少有一个很普遍的实现版本基于" class名称之字符串比较 ",如果你在四层深的单继承体系内的某个对象身上执行 dynamic_cast ,刚才说的那个实现版本所提供的每一次 dynamic_cast 可能会好用多达四次的strcmp调用,用以比较class名称。深度继承或多重继承的成本更高!

什么时候用 dynamic_cast 呢?用dynamic_cast通常是因为你想在一个认定的 派生类 对象身上执行 派生类操作函数,但手上却只有一个 指向基类 的指针或引用。

这里有两个做法来避免这个问题

> 使用容器并在其中存储直接指向派生类对象的指针(通常是智能指针),如此便消除了"通过基类接口处理对象"的需要。

用之前Window的例子,如果Window/SpecialWindow继承体系中只有SpecialWindow才支持闪烁效果,试着 不要 这样做:

class Window { ... };
class SpecialWindow : public Window  {
public:
  void blink();
  ...
};
typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs;
...
for( VPW::iterator iter = winPtrs.begin() ; iter != winPtrs.end() ; ++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() ; iter != winPtrs.end() ; ++iter )  {  // 没使用dynamic_cast
  (*iter)->blink();
}

当然,这种做法让你无法再同一个容器内存储指针"指向所有可能之各种Window派生类"。如果真要处理多种窗口类型,这就可能需要更多的容器,它们都必须具备类型安全性。

> 另一种做法可以 通过基类接口处理"所有可能之各种Window派生类",那就是在基类内提供virtual函数做你想对各个Window派生类做的事。接着用这个例子,虽然只有SpecialWindow可以闪烁,但或许将闪烁函数声明于基类内并提供一份"什么也没做"的缺省实现码是有意义的:

class Window  {
public:
  virtual void blink()  { }    // 缺省实现代码
  ...
};
class SpecialWindow : public Window {
public:
  virtual void blink() { ... };    // 在SpecialWindow类内,blink函数做一些动作
  ...
}
typedef std::vector< std::tr1::shared_ptr<Window> > VPW;    // 内含指针的容器,指向所有可能的Window类型
VPW winPtrs;
...
for( VPW::iterator iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter )    // 这里没使用 dynamic_cast
  (iter*)->blink();

不论哪一种写法——" 使用类型安全容器"或"将virtual函数往继承体系上方移动"——都并非放之四海皆准,但在许多情况下它们都提供一个可行的dynamic_cast替代方案。

3.注意

一定要避免一件事——连串 cascading dynamic_casts

也就是,类似这样的事情:

class Window  { ... };
...    // 派生类在这里定义
teypdef 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()) )  { ... }
  ...
}

这样的代码又大又慢,而且基础不稳,因为每次Window class继承体系一有所改变,所有这一类代码都必须再次检阅看看是否需要修改。

例如:一旦加入新的派生类,或许上述所有的连串判断中需要加入新的条件分支。这样的代码应该被“基于virtual函数调用”的取代。

4.总结

优良的C++代码很少使用转型,但若要说完全摆脱它们又不太可能。有很多转型是通情达理的,虽然有些并不是必须那样做。如同面对多种的构造函数那样,我们应该尽可能的少用转型动作,通常会把它隐藏在某个函数内,函数的接口会保护调用的人不受函数内部的干扰。

★ 请记住 ☆

? 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。

? 如果转型是必要的,试着将它隐藏于某个函数的背后。用户随后可以调用该函数,而不需要将转型放进他们自己的代码内。

? 宁可使用C++-style转型,不要使用旧式转型。新式转型容易辨识,并且有着分门别类的支持。

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

时间: 2024-10-21 16:24:29

《Effective C++》学习笔记——条款27的相关文章

effective c++学习笔记条款26-29

条款26:尽可能延后变量定义式的时间 1.中途抛出异常浪费构造函数 2.在循环内定义变量,消耗n个构造函数,n个析构函数:在循环外定义变量消耗n个赋值函数,1个构造,一个析构: 除非赋值的消耗比构造和析构少的不少,或者你处理的代码效率高度敏感,还是在循环内定义变量吧. 条款27:尽量少做转型动作 1.const_cast-----脱离常量属性,static_cast(隐式转换显示化),dynamic_cast(从一个寄放派生类的基类指针或引用调用派生类的成分),reinterpret_cats低

effective c++学习笔记条款23-25

条款23:宁可用非成员,非友元函数来替代成员函数 1.非成员函数提供了更好的封装性,这个函数内不能访问类的私有成员,封装的越严密我们对类的数据就可以弹性越大的操纵,因为可见这些数据的客户越少,反之数据影响的客户也就越少. 2.c++比较自然的做法-(关系到标准库numplace的组织结构),可以把不同便捷函数放到不同Namespace去,让客户来决定要用的非成员函数功能,这是类不能提供的. 条款24:若所有参数皆需类型转换,请为此采用非成员函数. 1.如果你需要为某个函数的所有参数(包括被thi

effective c++学习笔记条款11-13

条款11: 1.令赋值运算符返回一个&,因为STL,string都是这样做的,除非你有足够好的理由不这样做. 2.处理自我赋值的方法----(1).在没有成功获取对象数据时不要删除自己的数据,避免发生异常后原对象指针是一个悬浮指针 (2).判断自我赋值的检查操作会耗费不少时间,可以用swap交换数据技术来优化---(1)形参为赋值而来,(2)形参为静态引用,多加一个函数内拷贝操作.

effective c++学习笔记条款8-10

条款7:为多态基类声明虚析构函数 1.一个基类指针接受一个派生类对象的地址时,对该指针delete,仅仅释放基类部分 2.给所有类都带上虚析构函数是个馊主意,会带有vptr指向一个函数指针数组,扩大不必要的对象大小,除非补偿vptr,否则没有移植性. 3.string类和STL不含有虚析构函数,然而一些用户 却将他们作为基类,运用   delete指向派生类的基类指针,导致错误[c++11添加了禁止派生性质],他们不适合当基类. 4,手头上没有合适的纯虚函数,但你确实需要一个抽象类,把析构函数声

effective c++学习笔记条款20-22

条款20:用引用传递代替值传递 1.尽量以引用传递来代替传值传递,前者比较高效,并且可以避免切割问题 2.以上规则不适用于内置类型,以及STL的迭代器,和函数对象 条款21:必须返回对象时,别妄想返回对象的引用 1.绝对不要返回指针和引用指向一个局部对象或者静态局部对象而有可能需要多个这样的对象,条款4已经为在单线程环境合理返回&指向一个局部静态提供了一份设计实例.(保护初始化顺序) 条款22:将成员变量声明为private 1.切记将成员变量声明为private.这可赋予客户访问数据的一致性,

effective c++学习笔记条款4-7

条款4:确定对象被使用前已经初始化 一. 变量在不同情况下可能会初始化,也可能不会初始化. 注意初始化和赋值的区别. 1.在类中内置类型不会发生隐式初始化,自定义有默认构造函数的能被默认初始化 所以在构造类时务必初始化内置类型,最好给自定义的对象显示初始化避免在函数体中赋值浪费资源. 2.内置类型在函数体内不会初始化,在函数体外自动初始化为0. 二. 1.const和引用类型必须初始化,不可能赋值 三 1.当类实在是有较多构造函数,并且总是要对一些成员数据重复初始化,可以考虑将那些“赋值和初始化

effective c++学习笔记条款17-19

条款17:以独立语句将New对象放置入智能指针. 1.以独立语句将newed对象放置入智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露. void name(shared_ptr<管理对象类型>(new 管理对象类型),其它函数)),New被分配内存不一定马上放入管理对象,因为有其它函数干扰,这不是独立语句. 条款18:让接口容易被正确使用,不易被误用. 1.好的接口很容易被正确使用,不容易被误用.你应该在你的所有接口中努力达成这些性质. 2.“促进正确使用”的办法包括接

effective c++学习笔记条款35-37

#include<iostream> using namespace std; class A { public: void asd() { pri(); } private: /*virtual*/ void pri() { cout << "基类函数" << endl; } }; class B :public A { private: void pri() /*override*/ { cout << "派生类函数&quo

effective c++学习笔记条款29-31

条款29:为异常安全而努力是值得的[回顾] 1.异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏,这样的函数分为3种可能的保证:基本型,强烈型,不抛异常型 2.“强烈保证”往往能通过copying and swap 来实现出来,但并非所有函数都可实现或者具备现实意义. 3.函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全中”的最弱者.