条款27:尽量少做转型动作(Effective C++)

C++规则设计的目标之一是,保证类型错误决不可能发生。理论上如果你的程序很干净的通过编译,就表示它并不企图在任何对象身上执行任何不安全,无意义,愚蠢荒谬的操作。这是一个及其具有价值的保证,不要轻易放弃它。
但是在很多种情况下,我们不得不进行转型操作,转型操作破坏了类型系统。这可能会导致任何可能种类的麻烦,有些容易辨识,但是有些可能会很隐晦。所以在需要进行转型操作的时候一定要慎重,尽量通过设计避免不必要的转型操作。

类型转换的形式

首先我们回顾一下类型转换的语法,因为通常有三种不同的形式,可写出相同的类型转换动作。

  • C风格类型转换: (T)expression //将expression转换为类型T
  • 函数式风格类型转换: T(expression) //同上
    上面的两种形式并无差别,纯粹只是把小括号摆放的位置不同而已,我们称上述两种转为为”旧式转型”(old style cast)。

C++还提供四中新式转型(new style):

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

  • const_cast通常被用来将对象的常量性移除(cast away the constness)。它也是唯一有此能力的C++-style转型操作符。

  • dynamic_cast主要用来执行类型向下转型(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一一个可能耗费重大运行成本的转型动作
  • reinterpret_cast 意图执行低级转型,实际动作及结果可能取决于编译器。这也就表示它不可移植,例如将一个pointer to int 转型为int。这一类型转换在低级代码以外很少见。
  • static_cast 用来强迫隐式转换(implicit conversion),例如将non-const对象转换为const对象,或者将int转换为double等等。也可以用来执行上述多种转换的反向转换,例如将void指针转换为type指针,将pointer to derive 转化为point to bas。但是无法将const转换为non-const。

dynamic_cast与static_cast详解

static_cast是用来强迫隐式类型转换,它可以用于1.基本数据类型以及指针之间的转换;2.类层次中基类与子类成员函数指针的转换;3.类层次结构中基类与子类指针或者引用之间的转换。
dynamic_cast可以用于1.继承关系中类指针或者引用之间的转换;2.包含虚函数之间对象指针的转换3.以及保证转换的安全性。

static_cast

用于基本数据类型转换和指针之间的转换

1
2
3
4
5
6
7
8
char a;
int b = static_cast<int>(a);
char c = static_cast<char>(b);
char *pa = NULL;
int *pb = (int*)pa;
pb = static_cast<int*>(pa); //编译错误static_cast只能用于void指针和type指针之间的转换
void *pv = static_cast<void*>(pa); //正确
pb = static_cast<int*>(pv);

类层次中基类与子类成员函数指针的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class base {
public:
base(int t_data) : m_data(t_data) {}
void printData() { std::cout << m_data << std::endl; }
private:
int m_data;
};
class child : public base {
public:
child(int t_data) : base(t_data) {}
void printData() { std::cout << "this is in the child" << std::endl; }
};
typedef void (base::*basefun)();
int main() {
base a(10);
basefun func = &base::printData;
func = static_cast<basefun>(&child::printData);
(a.*func)(); //this is in the child
}

类层次结构中基类与子类指针或者引用之间的转换

上行转换:子类指针或引用转换为基类的指针或引用 —安全
下行转换:基类的指针或者引用转换为子类的指针或引用 —危险(避免这样做)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class A
{
};
class B:public A
{
};
class C:public A
{
};
class D
{
};
A objA;
B objB;
A* pObjA = new A();
B* pObjB = new B();
C* pObjC = new C();
D* pObjD = new D();
objA = static_cast<A&>(objB); //转换为基类引用
objA = static_cast<A>(objB);
objB = static_cast<B>(objA); //error 不能进行转换
pObjA = pObjB; //right 基类指针指向子类对象
//objB = objA; //error 子类指针指向基类对象
pObjA = static_cast<A*>(pObjB); //right 基类指针指向子类
pObjB = static_cast<B*>(pObjA); //强制转换 OK 基类到子类
//pObjC = static_cast<C*>(pObjB); //error 继承于统一类的派生指针之间转换
//pObjD = static_cast<D*>(pObjC); //error 两个无关联之间转换

dynamic_cast

继承关系的类指针对象或者引用之间的转换

若积累中没有虚函数,使用dynamic_cast可以将子类的指针或引用转换为基类的指针或引用,与static_cast用法相同,不同的是,这个时候使用dynamic_cast将基类指针转换为子类指针的时候会出现编译错误(static_cast不会,但是很危险)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class A
{
};
class B:public A
{
};
class C:public A
{
};
class D
{
};
A objA;
B objB;
A* pObjA = new A();
B* pObjB = new B();
C* pObjC = new C();
D* pObjD = new D();
//objA = dynamic_cast<A>(objB); //error 非引用
objA = dynamic_cast<A&>(objB);
//objB = dynamic_cast<B&>(objA); //error A 不是多态类型不能转换 若有虚函数则可以进行转换
pObjA = dynamic_cast<A*>(pObjB);
//pObjB = dynamic_cast<B*>(pObjA); //error A 继承关系 不是多态类型不能转换
//pObjB = dynamic_cast<B*>(pObjC); //error C 兄弟关系 不是多态类型不能转换
//pObjB = dynamic_cast<B*>(pObjD); //error D 没有关系 不是多态类型不能转换

包含有虚函数之间的对象指针的转换

使用dynamic_cast将基类指针转换为子类指针的时候并不是永远有效:只有基类指针本身指向的就是一个派生类对象的时候有效。其他时候结果为NULL;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A
{
Public:
Virtual ~A(){}
};
class B:public A
{
};
class C:public A
{
};
class D
{
Public:
Virtual ~D(){}
};
pObjB = dynamic_cast<B*>(pObjA); // worning 继承关系 父类具有虚函数 多态
pObjB = dynamic_cast<B*>(pObjD); //worning 没有关系 D是多态类型可以转换
//以上结果:pObjB == NULL 此处会发生一个运行时错误

dynamic_cast转换的安全性

当涉及到基类和派生类对象之间的转换的时候,总使用dynamic_cast会避免很多错误,它是安全的,但是它会给程序运行带来巨大的开销。
当子类指针转换为基类指针的时候,两种转型都OK,dynamic_cast开销较大。
当基类指针转换为派生类指针的时候,若基类中没有虚函数,static_cast不会报错,但是做法很危险,dynamic_cast编译不通过。当含有虚函数的时候,若基类指针没有指向派生类,这个时候会返回NULL,所以也是安全的。

虚函数对于dynamic_cast转换的作用

为什么dynamic_cast转换类指针的时候需要虚函数呢?
dynamic_cast转换是在运行时进行转换,运行时转换就需要知道类对象的信息(继承关系等)。
在运行时或者这个信息的是虚函数表指针,通过这个指针可以获取到该类对象的所有的虚函数,包括父类的。因为派生类会继承基类的虚函数表,所以通过这个虚函数表,我们就可以知道类对象的父类,在转换的时候就可以用来判断对象有无继承关系。
所以虚函数对于正确的基类指针转换为子类指针是非常重要的。

effective的三点建议

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。
  • 如果转型是必要的,试着将它隐藏与某个函数的背后。客户随后可以调用该函数,而不需要将转型放到他们自己的代码中。
  • 宁可使用C++-style转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌