22. 运行时类型信息
类层次导航
在软件开发中,尤其是在图形用户界面(GUI)编程中,经常需要在系统(如 GUI 库和操作系统)与应用程序之间传递对象。这些对象通常被称为小部件(widget)或控件(control)。由于系统不了解应用程序中对象的具体细节,类型信息在传递过程中可能会丢失。为了恢复这些丢失的类型信息,我们需要一种机制来在运行时检测对象的类型,这通常通过类型转换实现。
dynamic_cast
是 C++ 中用于类型转换的操作符,它能够在运行时检查对象的实际类型,并返回正确的类型指针。如果转换失败(即对象不是预期的类型),它会返回空指针。这种转换对于实现系统与应用程序之间的交互尤为重要,因为它允许应用程序安全地处理从系统传回的对象。
void my_event_handler(BBwindow* pw) {
if (auto pb = dynamic_cast<lval_box*>(pw)) {
// pw指向一个Ival_box吗?
// 使用Ival_box
int x = pb->get_value();
// ...其他操作
} else {
// 糟糕!处理意外情况…
}
}
pb = dynamic_cast<lval_box*>(pw)
的类型转换过程可以图示如下:
在运行时使用类型信息通常被称为“运行时类型信息”,简写为 RTTI
(Run-Time Type Information)。
从基类到派生类的转换通常称为向下转换(downcast
),因为我们画继承树的习惯是从根(基类)向下画。类似地,从派生类到基类的转换称为向上转换(upcast
)。而从基类到兄弟类的转换,例如从 BBwindow
转换为 Ival_box
,则称为交叉转换(crosscast
)。
dynamic_cast
运算符 dynamic_cast
接受两个运算对象:被 <
和 >
包围的一个类型和被 (
和 )
包围的一个指针或引用。我们首先看一个指针转换的例子:
dynamic_cast<t*>(p)
如果 p
是 T*
类型,或者是 D*
类型且 T
是 D
的基类,则得到的结果就如同我们简单地将 p
赋予一个 T*
一样。
dynamic_cast
不会允许意外地破坏对私有和保护基类的保护,了解到这一点还是会令人安心的。
class BBslider {}; // 基类
class lval_slider : public BBslider {}; // 派生类
class BB_ival_slider : public lval_slider, protected BBslider {}; // 多重继承
void f(BB_ival_slider* p) {
// 向上转换,直接赋值
lval_slider* pi1 = p; // 正确,无需dynamic_cast
// 向下转换,使用dynamic_cast
lval_slider* pi2 = dynamic_cast<lval_slider*>(p); // 正确,pi2将指向p
// 尝试将保护基类转换为指针,直接赋值是非法的
BBslider* pbb1 = p; // 错误,不能直接赋值
// 使用dynamic_cast进行向上转换,pbb2将成为空指针
BBslider* pbb2 = dynamic_cast<BBslider*>(p); // 正确,pbb2成为空指针
}
dynamic_cast
在用于向上转换时与简单赋值别无二致,这意味着使用 dynamic_cast 没有额外开销且对上下文是敏感的。
dynamic_cast
要求给定的指针或引用指向一个多态类型(有虚函数),以便进行向下或向上转换。
例如:
class Base { virtual void func() {} };
class Derived : public Base {};
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // OK
非多态:
class Base { void func() {} };
class Derived : public Base {};
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // 不OK
dynamic_cast
的实现依赖于对象的多态性。一个典型的实现会在对象上附加一个“类型信息对象”,通常是通过在对象类的虚函数表中放置指向类型信息的指针来实现的。这样,给定一个指向多态子对象的指针,就可以通过偏移量找到整个对象的起始地址。
dynamic_cast
的目标类型不必是多态的,这允许我们在多态类型中包含一个具体类型。
class Date {};
class lo_obj { // 对象I/O系统的基类
virtual lo_obj* clone() = 0;
};
class lo_date : public Date, public lo_obj {}; // 继承Date和lo_obj
void f(lo_obj* pio) {
Date* pd = dynamic_cast<Date*>(pio); // 正确使用dynamic_cast
}
向 void*
进行 dynamic_cast
可以用来确定一个多态类型对象的起始地址。
void* ptr = dynamic_cast<void*>(obj);
这种类型转换只有在与非常底层的函数(如处理 void*
的函数)打交道时才是有用的。用 dynamic_cast
将 void*
转换成其他类型也是不允许的(因为可能不知道去哪里找 vptr
)。
用 dynamic_cast 转换引用类型
与指针不同,引用类型转换不能返回 nullptr
,因为引用必须始终指向一个有效的对象。
二者对比:
- 指针转换:使用
dynamic_cast
转换指针时,如果转换失败,会返回nullptr
。因此,需要显式检查转换结果是否为nullptr
。 - 引用转换:使用
dynamic_cast
转换引用时,如果转换失败,会抛出一个std::bad_cast
异常。
#include <iostream>
#include <exception>
class Base {
public:
virtual ~Base() {} // 必须有虚函数才能使用 dynamic_cast
};
class Derived : public Base {};
class Unrelated {};
int main() {
try {
Base base;
Derived& derivedRef = dynamic_cast<Derived&>(base); // 尝试将 Base 引用转换为 Derived 引用
}
catch (const std::bad_cast& e) {
std::cerr << "转换失败: " << e.what() << std::endl;
}
return 0;
}
多重继承
如果在类层次中,一个类出现多次,则转换会失败(要么 nullptr,要么编译报错)
#include <iostream>
class A {
virtual void f(){}
};
class B : public virtual A {};
class C : public B{};
class D : public B{};
class E : public C, public D {};
int main() {
A* e = new E();
B* eb = dynamic_cast<B*>(e); // eb is nullptr
if (eb == nullptr) {
std::cout << "eb is nullptr" << std::endl;
}
else
{
std::cout << "eb is not nullptr" << std::endl;
}
E* ee = dynamic_cast<E*>(e); // ee is not nullptr
if (ee == nullptr) {
std::cout << "ee is nullptr" << std::endl;
}
else
{
std::cout << "ee is not nullptr" << std::endl;
}
B* eeb = dynamic_cast<B*>(ee); // 报错,有歧义
}
static_cast 和 dynamic_cast
dynamic_cast 的特性
-
dynamic_cast
可以在运行时检查多态虚基类到派生类或兄弟类的转换。 - 它要求运算对象是多态的,因为需要特定的信息来找到表示基类的子对象。
-
dynamic_cast
不能将void*
转换为其他类型,因为它需要探查对象的内部来确定其类型。
static_cast 的特性
-
static_cast
不进行运行时检查,因此适用于已知安全的转换。 - 它可以用于将
void*
转换为其他类型,因为编译器不对void*
指向的内存做任何假设。 -
static_cast
通常用于性能敏感的场景,因为它没有运行时开销。
class Receiver {
virtual void f() {}
};
class Storable {
virtual void g() {}
};
class Radio : public Receiver, public virtual Storable {};
void g(Radio& r) {
Receiver* prec = &r; // Receiver是Radio的普通基类
Radio* pr = static_cast<Radio*>(prec); // 正确,无须检查
pr = dynamic_cast<Radio*>(prec); // 正确,运行时检查
Storable* ps = &r; // Storable是Radio的虚基类
pr = static_cast<Radio*>(ps); // 错误:不能从虚基类转换
pr = dynamic_cast<Radio*>(ps); // 正确,运行时检查
}
dynamic_cast
和 static_cast
都遵守 const
规则和访问控制规则。例如,不能使用 dynamic_cast
或 static_cast
转换到私有基类,也不能强制去除 const
或 volatile
属性。如果需要去除 const
,可以使用 const_cast
。
构造与析构
类对象的构造和析构不仅仅是对内存区域的操作。构造函数负责在“裸内存”上创建对象,而析构函数则在对象生命周期结束时将其恢复到“裸内存”状态。这些操作遵循特定的顺序,以确保对象在使用前已经正确初始化,并且在使用完毕后才被销毁。
在构造过程中,对象的动态类型仅反映当前已经构造完成的部分。
在析构过程中,虚函数只会调用尚未销毁的部分。
最好避免在构造和析构过程中调用虚函数。
类型识别
在 C++ 中,dynamic_cast
运算符用于在运行时进行类型转换,但它通常只能处理程序员明确提及的类的派生情况。有时,我们需要获取一个对象的确切类型信息,比如对象类的名称或其布局,这时可以使用 typeid
操作符。
typeid
可以生成一个表示类型信息的对象,其声明类似于以下伪代码:
const type_info& typeid(expression);
#include <iostream>
#include <typeinfo>
class Shape {};
void f(Shape& r, Shape* p) {
const std::type_info& info1 = typeid(r); // 获得r引用的对象的类型
const std::type_info& info2 = typeid(*p); // 获得p指向的对象的类型
const std::type_info& info3 = typeid(p); // 获得指针的类型,即Shape*(这种用法不常见,通常是用错了)
std::cout << "r is: " << info1.name() << std::endl;
std::cout << "*p is: " << info2.name() << std::endl;
std::cout << "p is: " << info3.name() << std::endl;
}
int main() {
Shape x;
Shape* px = &x;
f(x, px);
return 0;
}
运行结果:
r is: class Shape
*p is: class Shape
p is: class Shape * __ptr64
如果 typeid()
的运算对象是一个值为 nullptr
的多态类型指针或引用,它会抛出一个 std::bad_typeid
异常。如果 typeid()
的运算对象不是多态类型或者不是一个左值,则结果在编译时即可确定,无须运行时对表达式求值。
如果运算对象是一个解引用的指针或引用,且其类型为多态类型,则返回的 type_info
对应对象的最底层派生类,即定义对象时使用的类型。例如:
#include <iostream>
#include <typeinfo>
class Base { virtual void foo() {} };
class Derived : public Base {};
int main() {
Base* b = new Derived;
std::cout << typeid(*b).name() << std::endl; // 输出 "Derived"
delete b;
return 0;
}
运行结果:
class Derived
RTTI 的使用和误用
运行时类型识别(RTTI)是 C++ 中用于确定对象运行时类型的一种机制,包括 dynamic_cast
和 typeid
。尽管 RTTI 在某些情况下非常有用,但过度依赖 RTTI 可能会导致代码效率低下和结构不良。
RTTI 应该在必要时才使用,静态(编译时)类型检查更安全,开销更小,且在适用的情况下,可以使程序结构更优。基于虚函数的接口结合了静态类型检查和运行时类型查询,保证了类型安全和灵活性。
RTTI 的误用示例
替换 switch
语句
RTTI 有时被误用来编写类似于 switch
语句的代码,这不仅代码丑陋,而且效率低下。
void rotate(const Shape& r) {
if (typeid(r) == typeid(Circle)) {
// 什么也不做
} else if (typeid(r) == typeid(Triangle)) {
// 旋转三角形...
} else if (typeid(r) == typeid(Square)) {
// 旋转正方形...
}
}
这种代码可以通过虚函数来改进,避免在运行时进行类型检查。
过度泛型化的容器
有时,程序员会创建一个过于泛型的 Object
类,强迫使用实现层的抽象,这可以通过模板来更好地解决。
class Object { /* ... */ };
class Container : public Object {
public:
void put(Object*);
Object* get();
};
class Ship : public Object { /* ... */ };
Ship* f(Ship* ps, Container* c) {
c->put(ps);
Object* p = c->get();
if (Ship* q = dynamic_cast<Ship*>(p)) {
return q;
} else {
// ...做一些其他事情(通常是错误处理)...
}
}
在这个例子中,Object
类是不必要的,因为它不是应用领域中的一个真实抽象。这个问题可以通过使用模板容器来解决,该容器只保存单一类型的指针。
std::vector<Ship*> c;
Ship* f(Ship* ps, std::vector<Ship*>& c) {
c.push_back(ps); // 将Ship存入容器
// ...
return c.back(); // 从容器中提取Ship
}