C++ Primer学习笔记 - 运行时类型识别

运行时类型识别(run-time type idenfitication,RTTI)的功能由2个运算符实现:
1)typeid,用于返回表达式的类型;
2)dynamic_cast,用于将base class pointer(或reference)安全转型为derived class pointer(或reference)。

TIPS:使用RTII时,应尽可能定义virtual函数,而非non-virtual函数。当调用virtual函数时,编译器会根据对象的动态类型自动地选择正确的函数版本。

dynamic_cast运算符

使用方法

// type必须是类型
dynamic_cast<type*>(e) // e必须是一个指针
dynamic_cast<type&>(e) // e必须是左值
dynamic_cast<type&&>(e) // e是右值

当e的类型满足以下3种条件之一时:1)是type本身,2)public base class,3)public derived class时,则转型可以成功;否则,转型失败。
当转换失败时,如果转换目标是指针,结果为0;如果转换目标是引用,则抛出bad_cast异常。

注:不能是private base/derived class,因为private继承代表has-a关系,而非public继承的is-a关系。

dynamic_cast指针转型

对于有public继承关系Base class和Derived class

class Base {
public:
	virtual void func1() { cout << "Base::func1()" << endl; }
	void func2() { cout << "Base::func2()" << endl; }
};

class Derived : public Base {
public:
	virtual void func1() { cout << "Derived::func1()" << endl; }
	void func2() { cout << "Derived::func2()" << endl; }
};

使用dynamic_cast这样对指针进行转型:

// 转型成功
Base* bp = new Derived;
if (Derived *dp = dynamic_cast<Derived*>(bp)) { // 转型
	// 使用dp指向Derived对象
	cout << "dp is not null" << endl; // 打印"dp is not null"
}
else {
	// bp指向base对象
	cout << "dp is null" << endl;
}

// 转型失败
Base* bp2 = new Base;
if (Derived *dp = dynamic_cast<Derived*>(bp2)) { // 转型
	// 使用dp指向Derived对象
	cout << "dp is not null" << endl;
}
else {
	// bp指向base对象
	cout << "dp is null" << endl;  // 打印"dp is null"
}

注:对空指针进行dynamic_cast转型,结果是所需类型的空指针。

dynamic_cast引用转型

与指针转型不同的是,引用不存在空引用的情况,转型失败时直接抛出std::bad_cast异常(定义在)。

void func(const Base &b)
{
	try {
		const Derived &d = dynamic_cast<const Derived&>(b); // 转型,注意dynami_cast转型无法丢掉const常量性
		cout << "b is Derived object" << endl;
	}
	catch (bad_cast) {
		cout << "b is Base object" << endl;
	}
}

int main()
{
	Base b;
	func(b); // 打印"b is Base object"

	Derived d;
	func(d); // 打印"b is Derived object"
	return 0;
}

[======]

typeid运算符

typeid运算符 运行程序向表达式提问:你的对象是什么类型?
typeid用法

typeid(e); // 运算符对象e,可以是任意表达式或类型的名字
// typeid 操作符结果是一个常量对象的引用,对象的类型是标准库类型type_info或其public派生类

typeid运算符的对象e可用是任意表达式,或者类型的名字,顶层const会被忽略。
如果e是引用,typeid返回所引用对象的类型;
如果e是数组或函数时,不会执行指针的标准类型转换,仍然是数组类型或者函数类型,而非指针类型;
如果e不属于类类型或者是一个不包含任何virtual函数的类时,typeid运算符指示的运算对象是静态类型。只有当运算符对象是 定义了至少一个virtual函数的类的左值对象时,typeid的结果直到运行时才会求得;否则,编译期就返回静态类型。

typeid的应用

typeid常用于比较2个表达式类型是否相同,或者比较一条表达式是否与指定类型相同

class Base {
public:
	virtual void func1() { cout << "Base::func1()" << endl; } // 包含了1个virtual函数
	void func2() { cout << "Base::func2()" << endl; }
};

class Derived : public Base {
public:
	virtual void func1() { cout << "Derived::func1()" << endl; }
	void func2() { cout << "Derived::func2()" << endl; }
};

int main()
{
	Derived *dp = new Derived;
	Base *bp = dp;
	if (typeid(bp) == typeid(dp)) { // 不通过:比较Base*和Derived*,将在编译期返回静态类型
		cout << "bp, dp own same type" << endl; // 不执行
	}

	if (typeid(*bp) == typeid(*dp)) { // 通过:比较Base对象和Derived对象,将在运行期求值
		cout << "*bp, *dp own same type" << endl; // 执行
	}

	if (typeid(*bp) == typeid(Derived)) { // 通过:比较Base对象和Derived对象,将在运行期求值
		cout << "*bp, Derived own same type" << endl; // 执行
	}

	if (typeid(bp) == typeid(Derived)) { // 不通过:比较Base*和Derived,将在编译期返回静态类型
		cout << "bp, Derived own same type" << endl; // 不执行
	}
	return 0;
}

[======]

运行时类型识别RTTI技术

何为两个对象相等?
对于2个对象,如果它们的\(\color{red}{类型相同}\)并且\(\color{red}{对应的数据成员相同}\),我们则称这2个对象是相等的。
在继承体系中,除了继承而来的数据成员,派生类还有自己负责添加的数据成员,因此,派生类的相等运算符(operator==)必须把派生类的新成员考虑进来。

一种判断对象是否相等的方案:定义一套virtual函数,在继承体系上的各个层次上分布执行相等性判断。为此,可以为base class定义一个相等运算符,可以将其工作委托给virtual函数equal。
上述方案存在的问题:virtual函数的base class版本和derived class版本必须具有相同的形参类型,否则无法通过base pointer/reference动态调用derived object的virtual函数。如果想定义virtual equal函数,参数必须是base class reference,因为base class并不知道derived class的存在,而equal判断是否相等如果用copy对象,就失去了意义。因此其函数参数必须是base class reference或pointer,那为什么是reference而不是pointer呢?因为参数不允许为null,跟null对象比较没有意义。

思考一个问题:如果两个对象的类型不一样,这2个对象相等吗?
答案是否定的,肯定不相等。这也就是说,比较2个对象是否相等时,如果排除了类型不同的情况,那么两个对象类型相同,而判断同类型对象是否相等,只需要借助对象的virtual函数equal比较数据成员(自身添加的以及继承而来的数据成员)即可。

于是,可以写出判断2个对象是否相等函数的示例:

class Base {
	// 并没有将operator==定义为成员函数,是因为不必要是成员函数。effective C++条款23告诉我们,这样可以增加类的封装性
	friend bool operator==(const Base&, const Base&);
public:
	// Base的接口成员
protected:
	virtual bool equal(const Base&) const;
private:
	// Base的数据成员
};

bool operator==(const Base& lhs, const Base& rhs)
{
	// 如果typeid不相同,返回false;否则调用equal比较数据成员
	return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}

class Derived : public Base {
public:
	// Derived的接口成员
protected:
	virtual bool equal(const Base&) const;
private:
	// Derived的数据成员
};

bool Derived::equal(const Base& rhs) const
{
	auto r = dynamic_cast<const Derived&>(rhs);

	// 执行比较2个Derived对象操作并返回结果
	if (all data == all data of rhs)
		return true;
	else return false;
}

type_info类

type_info类准确定义不同编译器略有不同。C++标准要求typeinfo类定义在头文件,且至少提供以下操作:

t1 == t2 如果type_info对象t1,t2是同一种类型,就返回true;否则,返回false
t1 != t2 如果type_info对象t1, t2不是同一种类型,就返回true;否则,返回false
t.name() 返回一个C风格字符串,表示类型的名字
t1.before(t2)

type_info类一般作为base class出现,当编译器希望提供额外的类型信息时,通常在type_info的derived class中完成。
可以从type_info的源码中看到,它没有默认构造函数,也删除了copy函数(copy构造、copy assignment运算符)。无法定义或拷贝type_info对象,也不能赋值。创建type_info类对象唯一途径是使用typeid运算符(前面有提到typeid运算符返回 表示表达式类型的值)

注意:C++ Primer 5th提到,type_info删除了移动构造函数,这点是通过删除private成员__std_type_info_data的移动构造函数实现,而非通过type_info声明自身的移动构造函数为delete。同样的,无法为type_info合成default构造函数,因为private成员__std_type_info_data的default构造函数已经被删除。

// 以下代码来自VS 2017 type_info定义
class type_info
{
public:
    type_info(const type_info&) = delete;
    type_info& operator=(const type_info&) = delete;
    virtual ~type_info() noexcept;
    ...
private:
    mutable __std_type_info_data _Data;
}

type_info的用法

如何利用type_info类打印类型信息?
type_info类的name成员函数返回一个C风格字符串,表示对象的类型名称。具体的名称字符串,取决于不同的编译器实现(可查看编译器手册),不一定与程序员编写的类型信息一样。
例,

int arr[10];
Derived d;
Base* p = &d;

cout << typeid(52).name() << endl;
cout << typeid(arr).name() << endl;
cout << typeid(std::string).name() << endl;
cout << typeid(p).name() << endl;
cout << typeid(*p).name() << endl;

MSVC 2017运行结果:

int
int [10]
class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >
class Base *
class Derived
posted @ 2021-12-01 16:44  明明1109  阅读(139)  评论(0编辑  收藏  举报