C++ RTTI 和四种类型转换

2021-03-02 15:58:35 +08:00
 yiouejv

RTTI 是 Run Time Type Information 的缩写,从字面上来理解就是执行时期的类型信息,其重要作用就是动态判别执行时期的类型。

并不是说这篇文章是 RTTI,和用于 RTTI 的四种类型转换,而是介绍 RTTI,再介绍一下 4 种类型转换,因为 RTTI 有用到其中一种类型转换,所以相当于两篇文章写在一起。

实际上 RTTI 用到的是typeid()dynamic_cast()

为什么会有 RTTI ?

C++是一种静态类型语言,其数据类型是在编译期就确定的,不能在运行时更改。然而由于面向对象程序设计中多态性的要求,C++中的指针或引用(Reference)本身的类型,可能与它实际代表(指向或引用)的类型并不一致。有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就产生了运行时类型识别的要求。

实事求是地讲,RTTI 是有用的。 但因为一些理论上及方法论上的原因,它破坏了面向对象的纯洁性。

首先, 它破坏了抽象,使一些本来不应该被使用的方法和属性被不正确地使用。

其次,因为运行时类型的不确定性,它把程序变得更脆弱。

第三点,也是最重要的一点,它使程序缺乏扩展性。 当加入了一个新的类型时,你也许需要仔细阅读你的dynamic_cast的代码,必要时改动它们,以保证这个新的类型的加入不会导致问题。 而在这个过程中,编译器将不会给你任何帮助。

总的来说,RTTI 因为它的方法论上的一些缺点,它必须被非常谨慎地使用。 今天面向对象语言的类型系统中的很多东西就是产生于避免 RTTI 的各种努力。


首先我们来个例子感受一下:

#include<iostream>
#include<typeinfo>
using namespace std;

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

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

void funcC(Base* p)
{
    Derived* dp = dynamic_cast<Derived*>(p);
    if (dp != NULL) {
        dp->funcB();
    }
    else {
        p->funcA();
    }
};

void funcD(Base* p)
{
    Derived* dp = NULL;
    if (typeid(*p) == typeid(Derived))
    {
        dp = static_cast<Derived*>(p);
        dp->funcB();
    }
    else {
        p->funcA();
    }
}

int main(int argc, char const* argv[])
{
    Base* p = new Derived;
    cout << typeid(p).name() << endl;
    cout << typeid(*p).name() << endl;
    funcD(p);
    funcC(p);
    delete p;

    Base* dp = new Base;
    funcC(dp);
    funcD(dp);
    delete dp;
    return 0;
}

funcC 是用dynamic_cast类型转换是否成功来识别类型的,dynamic_cast操作符将基类类型对象的引用或指针转化为同一继承层次中的其他类型的引用或指针。

如果绑定到引用或指针的对象不是目标类型的对象,则dynamic_cast失败。
如果转换到指针类型的dynamic_cast失败,则dynamic_cast的结果是NULL值;
如果转换到引用类型的dynamic_cast失败, 则抛出一个bad_cast类型的异常

funcD 是用typeid判断基类地址是否一致的办法来识别类型的。


typeid

下面我们具体说说 typeid

typeid是 C++的关键字之一, 等同于sizeof这类 operator 。typeid 操作符的返回结果是名为 type_info的标准库类型的对象的引用,在头文件 typeinfo 中定义。 有两种形式:

  1. typeid(type)
  2. typeid(expression)

表达式的类型是类类型,且至少含有一个虚函数,则 typeid 操作符返回表达式的动态类型,需要在运行时计算,否则返回表达式的静态类型,在编译时就可以计算。

C++标准规定了其实现必须提供如下四种操作:

  1. t1 == t2: 如果两个对象 t1 和 t2 类型相同,则返回 true;否则返回 false
  2. t1 != t2: 如果两个对象 t1 和 t2 类型不同,则返回 true;否则返回 false
  3. t.name(): 返回类型的 c-style 字符串,类型名字用系统相关的方法产生
  4. t1.before(t2): 返回指出 t1 是否出现在 t2 之前 的 bool 值

type_info类提供了 public 虚析构函数,以使用户能够用其作为基类。它的默认构造函数和复制构造函数及赋值操作符都定义为 private,所以不能定义或复制 type_info 类型的对象。 程序中创建 type_info 对象的唯一方法是使用 typeid 操作符。 由此可见,如果把 typeid 看作函数的话,其应该是 type_info 的友元。 这具体由编译器的实现所决定,标准只要求实现为每个类型返回唯一的字符串。

例 1: C++里面的 typeid 运箕符返回值是什么?

答: 常量对象的引用。

如果 p 是基类指针,并且指向一个派生类型的对象,并且基类中有虚函数,那么typeid(*p)返回 p 所指向的派生类类型,typeid(p)返回基类类型。

RTTI 的实现原理: 通过在虚表中放一个额外的指针,每个新类只产生一个 typeinfo 实例,额外指针指向 typeinfo, typeid 返回对它的一个引用。


static_cast

static_cast < new_type > ( expression )

本来应该先讨论dynamic_cast的,因为咱们本来聊的 RTTI 嘛,但是先了解一下static_cast,然后看和dynamic_cast的比较可能更好一点。

使用场景:

  1. 基本数据类型之间的转换
  2. initializing conversion
int n = static_cast<int>(3.14);
cout << "n = " << n << '\n';
vector<int> v = static_cast<vector<int>>(10);
cout << "v.size() = " << v.size() << '\n';
  1. 类指针或引用向下转换。
  2. 将类型转为右值类型,进行 move 操作,这个在标准库中有体现(放在下一篇文章中解释)
vector<int> v = static_cast<vector<int>>(10);
vector<int> v2 = static_cast<vector<int>&&>(v);
cout << "after move, v.size() = " << v.size() << '\n';
cout << v.size() << endl;
  1. 子类数组指针向上转成父类的指针
struct B {
    int m = 0;
    void hello() const {
        cout << "Hello world, this is B!\n";
    }
};
struct D : B {
    void hello() const {
        cout << "Hello world, this is D!\n";
    }
};

D a[10];
B* dp = static_cast<B*>(a);
dp->hello();
  1. 枚举转换成 int or float
enum E { ONE = 1, TWO, THREE };
E e = E::ONE;
int one = static_cast<int>(e);
cout << one << '\n';
  1. int to enum, enum to another emum
enum class E { ONE = 1, TWO, THREE };
enum EU { ONE = 1, TWO, THREE };

E e = E::ONE;
int one = static_cast<int>(e);
E e2 = static_cast<E>(one);
EU eu = static_cast<EU>(e2);
  1. void* to any type
int a = 100;
void* voidp = &a;
int *p = static_cast<int*>(voidp);

注意:

  1. static_cast 不能转换掉 expression 的 const 、volatile 和__unaligned 属性,
  2. 编译器隐式执行任何类型转换都可由 static_cast 显示完成

dynamic_cast

dynamic_cast < new-type > ( expression )

接下来是dynamic_cast:

动态映射可以映射到中间层级,将派生类映射到任何一个基类,然后在基类之间可以相互映射。

dynamic_cast 实现原理: 先恢复源指针的 RTTI 信息,再取目标的 RTTT 信息,比较两者是否相同,不同取目标类型的基类; 由于它需要检查一长串基类列表,故动态映射的开销比 typeid 大。

dynamic_cast 的安全性: 如实现原理所说,dynamic_cast会做一系列的类型检查,转换成功会返回目标类型指针,失败则会返回 NULL, 相对于static_cast安全,因为 static_cast即使转换失败也不会返回 NULL 。

例 2: 这种情况下 static_cast() 也是安全的。

class Base
{
public:
    void func() { cout << "Base func" << endl; }
};

class Derived : public Base
{
public:
    void func() { cout << "Derived func" << endl; }
};

int main(int argc, char const* argv[])
{
    Derived *pd = new Derived;
    pd->func();
    Base* pb1 = dynamic_cast<Base*>(pd);
    pb1->func();
    Base* pb2 = static_cast<Base*>(pd);
    pb2->func();
    return 0;
}

pd 指针指向的内存是子类对象,我们知道,继承子类是包含父类的,相当于在父类的基础上在添加子类的成员(如果你还不清楚的话,建议你看一下我之前的文章: 虚函数,虚表深度剖析)。 所以 pd 指针转成父类指针也是没问题的,static_cast也一样安全。

相反,如果指针指向的内存是父类成员,转成子类指针,dynamic_cast 则会失败,返回 NULL, 但是static_cast不会失败,强制转过去了,如果此时子类指针访问父类中不存在,但是子类中存在的成员,则会发生意想不到的问题。

看下面这个例子:

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

class Derived : public Base
{
public:
    virtual void func() { cout << "Derived func" << endl; }
    int m_value = 0;
};

int main(int argc, char const* argv[])
{
    Base *pb = new Base;
    pb->func();
    Derived* pd1 = dynamic_cast<Derived*>(pb);
    if (pd1 != NULL) {
        pd1->func();
    } else {
        cout << "dynamic_cast failed" << endl;
    }
    Derived* pd2 = static_cast<Derived*>(pb);
    pd2->func();
    cout << "m_value: " << pd2->m_value << endl;
    return 0;
}

输出:

Base func
dynamic_cast failed
Base func  // 父类中也有这个虚函数,所以 static_cast 转换调用没出问题。
m_value: -33686019  // 这里出问题了

对于上行转换,static_castdynamic_cast 效果一样,都安全, 如果只是单纯的向上转的话,没必要,直接用虚函数实现就好了。

对于下行转换: 你必须确定要转换的数据确实是目标类型的数据,即需要注意要转换的父类类型指针是否真的指向子类对象,如果是,static_cast 和 dynamic_cast 都能成功;如果不是 static_cast 能返回,但是不安全,可能会出现访问越界错误,而 dynamic_cast 在运行时类型检查过程中,判定该过程不能转换,返回 NULL 。


const_cast

const_cast < new_type > ( expression )

上面讲了static_cast是不能去掉 const,而 const_cast是专门用来去掉 const

而添加 const, static_cast 也是可以添加上 const,只是不能去掉 const

看下面一个例子:

const int a = 26;
const int* pa = &a;
//*pa = 3;  // 编译不过,指针常量不能通过指针修改值

int* b = const_cast<int*>(pa);  // 把 const 转换掉
*b = 3;

cout << "a: " << a << endl;  // 26
cout << "*b: " << *b << endl;  // 3

a 为 const int 类型,不可修改,pa 为 const int* 类型,不能通过 pa 指针修改 a 的值

b 通过 const_cast 转换掉了 const,成功修改了 a 的值。

有一个问题,为什么 a 输出是 26 呢?

如果存在const int x = 26; 这种情况,那么编译器会认为 x 是一个编译期可计算出结果的常量,那么 x 就会像宏定义一样,用到 x 的地方会被编译器替换成 26 。

上述这个例子不建议使用,因为 a 声明为 const int 类型,实际上是并不希望被修改的,这样强行修改可能会导致项目里不可预期的错误。

const_cast 的使用场景

  1. 如果有一个函数,它的形参是 non-const 类型变量,而且函数不会对实参的值进行改动,这时我们可以使用类型为 const 的变量来调用函数。
void func(int* a)
{ }

int main()
{
    const int a = 26;
    const int* pa = &a;
    func(const_cast<int*>(pa));
}

这种情况其实我觉得没必要,实际上我不想改的话,我形参加 const,把前提推翻不就行了,还安全。

  1. 定义了一个 non-const 的变量,却使用了一个指向 const 值的指针来指向它(这不是没事找事嘛),在程序的某处我们想改变这个变量的值了,但手头只持有指针,这时const_cast就可以用到了
int a = 26;
const int* pa = &a;
// *pa = 1; 编译不过
int* pa2 = const_cast<int*>(pa);
*pa2 = 1;

reinterpret_cast

reinterpret_cast < new_type > ( expression )

reinterpret_ cast 通常为操作数的位模式提供较低层的重新解释。

看这个例子:

int n = 9; double d = static_cast<double>(n); 
cout << n << " " << d; // 输出 9 9

int n2 = 9; double d2 = reinterpret_cast<double&>(n2);
cout << n2 << " " << d2; // 输出 9 -9.25596e+61

上面的例子中,我们将一个变址从 int 转换到 double 。这些类型的二进制表达式是不同的。要将整数 9 转换到双精度整数 9, static_cast 需要正确地为双精度整数 d 补足比特位。其结果为 9.0 。

而 reinterpret_cast 的行为却不同,仅仅是把内存拷贝到目标空间,解释出来是一个大数。

reinterpret_cast 这个操作符被用于的类型转换的转换结果几乎都是未知的。

使用 reinterpret_cast 的代码很难移植。转换函数指针的代码是不可移植的,( C++不保证所有的函数指针都被用一样的方法表示),在一些情况下这样的转换会产生不正确的结果。 所以应该避免转换函数指针类型,按照 C++新思维的话来说,reinterpret_cast 是为了映射到一个完全不同类型的意思,这个关键词在我们需要把类型映射回原有类型时用到它。 我们映射到的类型仅仅是为了故弄玄虚和其他目的,这是所有映射中最危险的。reinterpret_cast 就是一把锐利无比的双刃剑,除非你处于背水一战和火烧眉毛的危急时刻,否则绝不能使用。

其实 reinterpret_cast 用法细节还有不少,什么时候需要用到,再去官方了解一下就好了,现在纠的太细意义不大。

1693 次点击
所在节点    C++
2 条回复
chesterzzy
2021-03-03 10:09:10 +08:00
讲得挺好的,已关注公众号
yiouejv
2021-03-03 10:22:52 +08:00
@chesterzzy 嘿嘿,一起学习

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/757675

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX