Item 27:最小化类型转换

C++的类型检查只在编译时执行,运行时没有类型错误的概念。 理论上讲只要你的代码可以编译那么就运行时就不会有不安全的操作发生。 但 C++ 允许类型转换,也正是类型转换破坏了理论上的类型系统。

C++ 中的类型转换

旧风格的强制类型转换

  • C 风格的类型转换:
(T) expression
  • 函数风格的类型转换:
T(expression)

以上两种形式之间没有本质上的不同,它纯粹就是一个把括号放在哪的问题。我把这两种形式称为旧风格的强制转型。

C++ 中的四种新的强制类型转换

C++ 同时提供了四种新的强制转型形式,通常称为新风格的或 C++ 风格的强制转型:

const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
  • const_cast 一般用于强制消除对象的常量性。它是唯一能做到这一点的 C++ 风格的强制转型。
  • dynamic_cast 主要用于执行“安全的向下转型(safe downcasting)”,也就是说,要确定一个对象是否是一个继承体系中的一个特定类型。它是唯一不能用旧风格语法执行的强制转型。也是唯一可能有重大运行时代价的强制转型。
  • reinterpret_cast 是特意用于底层的强制转型,导致实现依赖(就是说,不可移植)的结果,例如,将一个指针转型为一个整数。这样的强制转型在底层代码以外应该极为罕见。
  • static_cast 可以被用于强制隐型转换
    • non-const 对象转型为 const 对象
    • int 转型为 doubl
    • void* 指针转型为有类型指针
    • 基类指针转型为派生类指针
    • 但是它不能将一个 const 对象转型为 non-const 对象(只有 const_cast 能做到)

为什么用C++风格类型转换?

C 风格转型和函数风格转型没有区别,只是括号的位置不一样。C++风格的类型转换语义更加明确(编译器会做更详细的检查)不容易误用, 另外也更容易在代码中找到那些破坏了类型系统的地方。所以尽量用C++风格的转型,比如下面代码中,后者是更好的习惯:

func(Widget(15));
func(static_cast<Widget>(15));

类型转换做了什么?

很多人认为类型转换只是告诉编译器把它当做某种类型。事实上并非如此,比如最常见的数字类型转换:

int x,y;
double d = static_cast<double>(x)/y;

上述的类型转换一定是产生了更多的二进制代码,因为多数平台中 int 和 double 的底层表示并不一样。再来一个例子:

Derived d;        // 子类对象
Base *pb = &d;    // 父类指针

同一对象的子类指针和父类指针有时并不是同一地址(取决于编译器和平台),而运行时代码需要计算这一偏移量。 一个对象会有不同的地址是C++中独有的现象,所以不要对C++对象的内存分布做任何假设,更不要基于该假设做类型转换。 这样可以避免一些未定义行为。

需要转型吗?

C++ 类型转换有趣的一点在于,很多时候看起来正确事实上却是错误的。比如 SpecialWindow 继承自 Window, 它的 onResize 需要调用父类的 onResize。一个实现方式是这样的:

class SpecialWindow: public Window{
public:
    virtual void onResize(){
        // Window onResize ...
        static_cast<Window>(*this).onResize();
 
        // SpecialWindow onResize ...
    }
};

这样写的结果是当前对象父类部分被拷贝(调用了Window的拷贝构造函数),并在这个副本上调用 onResize。 当前对象的 Window::onResize 并未被调用,而 SpetialWindow::onResize 的后续代码被执行了, 如果后续代码修改了属性值,那么当前对象将处于无效的状态。正确的方法也很显然:

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

dynamic_cast 的性能问题

在一般的实现中 dynamic_cast 会逐级地比较类名。比如4级的继承结构,dynamic_cast<Base> 将会调用4次strcmp 才能确定最终的那个子类型。 所以在性能关键的部分尽量避免 dynamic_cast。通常有两种途径:

  • 使用子类的容器,而不是父类容器。比如:
vector<Window> v;
dynamic_cast<SpecialWindow>(v[0]).blink();

换成子类容器就好了。

vector<SpecialWindow> v;
v[0].blink();

但这样你就不能在容器里放其他子类的对象了,你可以定义多个类型安全的容器来分别存放这些子类的对象。

  • 通过虚函数提供统一的父类接口。比如:
class Window{
public:
    virtual void blink();
    ...
};
class SpecialWindow: public Window{
public:
    virtual void blink();
    ...
};
vector<Window> v;
v[0].blink();

这两个方法并不能解决所有问题,但如果可以解决你的问题,你就应该采用它们来避免类型转换。 这取决于你的使用场景,但可以确定的是,连续的 dynamic_cast 一定要避免,比如这样:

if(SpecialWindow1 *p = dynamic_cast<SpecialWindow1*>(it->get()){...}
else if(SpecialWindow2 *p = dynamic_cast<SpecialWindow2*>(it->get()){...}
else if(SpecialWindow3 *p = dynamic_cast<SpecialWindow3*>(it->get()){...}
...

这样的代码性能极差,而且又代码维护问题:当你又来一个 SpecialWindow4 的时候你需要再次找到这段代码来进行扩展。 使用虚函数完全可以替代上述的实现。

总结

  • 避免强制转型的随时应用,特别是在性能敏感的代码中应用 dynamic_casts,如果一个设计需要强制转型,设法开发一个没有强制转型的侯选方案。
  • 如果必须要强制转型,设法将它隐藏在一个函数中。客户可以用调用那个函数来代替在他们自己的代码中加入强制转型。
  • 尽量用 C++ 风格的强制转型替换旧风格的强制转型。它们更容易被注意到,而且他们做的事情也更加明确。
posted @ 2020-02-09 20:43  刘-皇叔  阅读(97)  评论(0编辑  收藏  举报