RTTI


RTTI,即运行时类型识别,由两个运算符实现:

  • typeid运算符,返回表达式的类型
  • dynamic_cast运算符,将基类指针或引用安全地转换成派生类的指针或引用

本篇主要阐述typeid运算符

typeid运算符

typeid表达式的形式是typeid(e),其中e可以是任意表达式或类型的名字,操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或其公有派生类。

typeid运算符的一些特点:

  • 忽略顶层const
  • 如果表达式是引用类型,返回其所引用对象的类型,而不是type reference
  • 作用于函数或数组时不会返回指针类型
  • 当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid运算符指示的是运算符对象的静态类型。例如,指向父类的指针和引用父类的引用,均返回其声明类型
  • 当运算对象定义了至少一个虚函数,其类型直到运行时才会求得

typeid运算符使用示例程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main()
{
// case 1: ignore top-const
const int ci = 0;
int i = 0;
cout << "type of ci: " << typeid(ci).name() << endl;
cout << "type of i: " << typeid(i).name() << endl;
assert(typeid(ci) == typeid(i));

// case 2: reference
double d = 0.9;
double &rd = d;
cout << "type of d: " << typeid(d).name() << endl;
cout << "type of rd: " << typeid(rd).name() << endl;
assert(typeid(d) == typeid(rd));

// case 3: function and array
int a[10];
cout << "type of array int[10]: " << typeid(a).name() << endl;
cout << "type of fucntion main: " << typeid(main).name() << endl;
cout << "type of pointer to int: " << typeid(int *).name() << endl;

return 0;
}

输出结果为:

1
2
3
4
5
6
7
type of ci: i
type of i: i
type of d: d
type of rd: d
type of array int[10]: A10_i
type of fucntion main: FivE
type of pointer to int: Pi

当typeid作用于类类型时,基类和派生类是否具备虚函数会对结果有影响,我们使用如下两个类:base和derived验证。

1
2
3
4
5
6
7
8
9
class base
{
public:
/*virtual*/ ~base() {}
};

class derived : public base
{
};

base中的虚析构函数被注释掉了,执行如下main函数

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
int main()
{
// case 4: non-contain virtual function class
// pointer
base *p1 = new base;
base *p2 = new derived;
derived *p3 = new derived;

cout << "type of *p1: " << typeid(*p1).name() << endl;
cout << "type of *p2: " << typeid(*p2).name() << endl;
cout << "type of *p3: " << typeid(*p3).name() << endl;

// reference
base b1;
derived d1;
base &rb1 = b1;
base &rb2 = d1;
derived &rb3 = d1;

cout << "type of rb1: " << typeid(rb1).name() << endl;
cout << "type of rb2: " << typeid(rb2).name() << endl;
cout << "type of rb3: " << typeid(rb3).name() << endl;

return 0;
}

输出结果如下(忽略了构造函数和析构函数的输出)

1
2
3
4
5
6
type of *p1: 4base
type of *p2: 4base
type of *p3: 7derived
type of rb1: 4base
type of rb2: 4base
type of rb3: 7derived

取消对base虚析构函数的注释,再次运行:

1
2
3
4
5
6
type of *p1: 4base
type of *p2: 7derived
type of *p3: 7derived
type of rb1: 4base
type of rb2: 7derived
type of rb3: 7derived

Note:typeid作用于指针上只会输出静态类型信息,不能获取其指向对象是什么类型。比如下面的测试代码,其中base和derived中包含有虚函数:

1
2
3
4
5
6
base *p1 = new base;
base *p2 = new derived;
derived *p3 = new derived;
cout << "type of p1: " << typeid(p1).name() << '\t' << "type of *p1: " << typeid(*p1).name() << endl;
cout << "type of p2: " << typeid(p2).name() << '\t' << "type of *p2: " << typeid(*p2).name() << endl;
cout << "type of p3: " << typeid(p3).name() << '\t' << "type of *p3: " << typeid(*p3).name() << endl;

输出结果为:

1
2
3
type of p1: P4base	type of *p1: 4base
type of p2: P4base type of *p2: 7derived
type of p3: P7derived type of *p3: 7derived

typeid是否需要运行时检查决定了表达式是否会被求值。只有当类型含有虚函数时,编译器才会对表达式求值,反之,如果类型不含有虚函数,typeid返回表达式的静态类型。如果表达式的动态类型可能与静态类型不同,必须在运行时对表达式求值以确定返回的类型。因此,若指针指向的类型不含有虚函数,则其不必非得是有效指针。

使用RTTI

当设计类的时候,可能类中需要定义某个针对该类类型的对象的操作,则完成该操作的方法的参数一般是该类的类型。例如,为base类实现相等运算符。

1
2
3
4
5
6
7
8
9
class base
{
public:
base()
{
cout << " \n base constructor \n";
}
bool operator==(const base &);
};

当设计一个derived类继承自base时,我们可能也需要为derived定义相等运算符。但虚函数的参数必须是相符的,因此derived类的定义看起来可能是这样的:

1
2
3
4
5
6
7
8
9
class derived : public base
{
public:
derived()
{
cout << " \n derived constructor \n";
}
bool operator==(const base &);
};

此时在derived中的相等运算符中,存在两个问题:

  • 我们无法访问base类中的数据成员a,因为他们是私有的

  • 我们无法对比derived中的b成员,编译器会提醒我们class “base” has no member “b”

即使我们可以通过在base类中添加一个方法返回该私有成员,但第二个问题却难以解决。一个方法是在derived类的相等运算符方法中强制转换类型。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class base
{
public:
base() = default;
base(int _a) : a(_a)
{
}
virtual bool operator==(const base &rhs) const
{
return a == rhs.a;
}

virtual bool operator!=(const base &rhs) const
{
return a != rhs.a;
}

int get_a() const { return a; } // 提供访问基类私有成员的方法

private:
int a{};
};

class derived : public base
{
public:
derived() = default;
derived(int _a, int _b) : base(_a), b(_b)
{
}
bool operator==(const base &rhs) const
{
// 强制类型转换,但我们需要确保传入的参数动态类型确实是derived
return base::operator==(rhs) &&
b == (static_cast<const derived &>(rhs)).b;
}

bool operator!=(const base &rhs) const
{
return !(*this == rhs);
}

private:
int b{};
};

倘若我们不小心比较了一个base类对象和derived类对象,可能最终结果是符合我们的预期,但是该过程是未定义的。要想实现真正有效的相等比较操作,首先我们需要认清楚一个事实:如果参与对比的两个对象类型不同,则比较结果为false。因此我们可以通过RTTI解决该问题:定义相等运算符的形参是基类的引用,使用typeid检查两个运算对象是否是同一类型,类型一致才继续对比成员,并通过dynamic_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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class base
{
friend bool operator==(const base &lhs, const base &rhs)
{
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}

friend bool operator!=(const base &lhs, const base &rhs)
{
return !(lhs == rhs);
}

public:
base() = default;
base(int _a) : a(_a)
{
}
virtual bool equal(const base &rhs) const
{
return a == rhs.a;
}

private:
int a{};
};

class derived : public base
{
public:
derived() = default;
derived(int _a, int _b) : base(_a), b(_b)
{
}
bool equal(const base &rhs) const
{
auto r = dynamic_cast<const derived &>(rhs);
return base::equal(rhs) && b == r.b;
}

private:
int b{};
};

int main()
{
// case 1: base != derived
derived d1(1, 2);
base b1(1);
assert(d1 != b1);

// case 2: base != base
base b2(1);
base b3(2);
assert(b2 != b3);

// case 3: base == base
base b4(13);
base b5(13);
assert(b4 == b5);

// case 4: derived != derived
derived d2(11, 12);
derived d3(11, 10);
assert(d2 != d3);

// case 4: derived == derived
derived d4(11, 12);
derived d5(11, 12);
assert(d4 == d5);

cout << "Pass!\n";

return 0;
}

运行结果为:

1
Pass!

type_info类

type_info类的精确定义随着编译器的不同而略有差异,但c++标准规定必须定义在typeinfo头文件中,且至少提供如下4种操作:

操作 含义
t1 == t2 是否type_info对象t1和t2表示同一种类型
t1 != t2 是否type_info对象t1和t2表示不同类型
t.name() 返回一个c风格的字符串,表示类型名字的可打印形式
t1.before(t2) 返回一个bool值,表示t1是否位于t2之前

需要注意的是,type_info对象只能由typeid运算符创建。如下程序测试c++ primer练习19.10:

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
31
32
33
class A
{
public:
virtual ~A() {}
};

class B : public A
{
public:
virtual ~B() {}
};

class C : public B
{
public:
virtual ~C() {}
};

int main()
{
A *pa = new C;
cout << typeid(pa).name() << endl;

C cobj;
A &ra = cobj;
cout << typeid(&ra).name() << endl;
B *px = new B;
A &rra = *px;
cout << typeid(rra).name() << endl;

delete px;
return 0;
}

运行结果如下:

1
2
3
P1A
P1A
1B