C++中RTTI实现原理

目录

1.引言

2.typeid

2.1.虚函数表(vtable)

2.2.类型信息(type_info)

3.dynamic_cast

4.缺陷

5.一些库/软件提供的RTTI实现

5.1. CATIA的RTTI

5.2. QT的RTTI

5.3. FreeCAD的RTTI

6.实例

7.总结


1.引言

        RTTI是Runtime Type Identification的缩写,意思是运行时类型识别。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。但是现在RTTI的类型识别已经不限于此了,它还能通过typeid操作符识别出所有的基本类型(int,指针等)的变量对应的类型。

        C++提供了typeiddynamic_cast两个运算符(而非函数)关键字来提供动态类型信息和动态类型转换,使用需要在编译器选项中指定-rtti(clang和gcc等都默认开启)。关闭则可以设置选项-fno-rtti;微软的编译器仅当指定了 /GR(启用运行时类型信息)编译选项时,才会为多态类生成类型信息。

2.typeid

        typeid运算符,该运算符返回其表达式或类型名的实际类型。即返回一个类型为std::type_info的对象的const引用。type_info是std中的一个类,它用于记录与类型相关的信息。类type_info的定义大概如下:

class type_info {
public:
    type_info(const type_info& rhs) = delete; // cannot be copied
    virtual ~type_info();
    size_t hash_code() const;
    _CRTIMP_PURE bool operator==(const type_info& rhs) const;
    type_info& operator=(const type_info& rhs) = delete; // cannot be copied
    _CRTIMP_PURE bool operator!=(const type_info& rhs) const;
    _CRTIMP_PURE int before(const type_info& rhs) const;
    size_t hash_code() const noexcept;
    _CRTIMP_PURE const char* name() const;
    _CRTIMP_PURE const char* raw_name() const;
};

        从上面的定义也可以看到,type_info提供了两个对象的相等比较操作,但是用户并不能自己定义一个type_info的对象,而只能通过typeid运算符返回一个对象的const引用来使用type_info的对象。因为其只声明了一个构造函数(复制构造函数)且为private,所以编译器不会合成任何的构造函数,而且赋值操作运行符也为private。这两个操作就完全禁止了用户对type_info对象的定义和复制操作,用户只能通过指向type_info的对象的指针或引用来使用该类。

        C++标准并不涉及具体实现的细节,而更侧重于定义语言的语法和语义。因此typeid的性能就由具体编译器实现所决定。然而,一般而言,typeid的实现通常基于虚函数表(vtable)和类型信息(type_info)。以下是typeid的典型实现方式:

2.1.虚函数表(vtable)

        对于含有虚函数的类,每个对象都包含一个指向虚函数表的指针。虚函数表中存储了该类及其所有基类的虚函数的地址。typeid可以通过访问这个虚函数表,找到存储类型信息的部分。

2.2.类型信息(type_info)

        每个类的type_info对象包含有关该类的类型信息,例如类名等。这个信息通常被存储在只读数据区域。type_info的实现可能包含了一个字符串或其他标识符,以表示该类型。

        typeid运算符用于获取对象的类型信息。它返回一个type_info对象,包含有关类型的信息,如类型名称。

        具体示例如下(vs2019环境下编译生成结果):

// 具有virtual函数 }
class CM {
public:
	virtual void func() {}
};
// 具有virtual函数}
class CMM : public CM {
};
// 没有virtual函数
class CN {};

//打印有虚函数的实际类型
void printTypeInfo(const CM* pm)
{
	qDebug() << typeid(pm).name();
	qDebug() << typeid(*pm).name();
}

int main()
{
    int n = 0;
	CM a;
	CMM aa;
	CN b;
	CN* pB = &b;

	// int和AA都是类型名
	qDebug() << typeid(int).name();
	qDebug() << typeid(CMM).name();
	// n为基本变量
	qDebug() << typeid(n).name();
	// aa所属的类虽然存在virtual,但是aa为一个具体的对象
	qDebug() << typeid(aa).name();
	// pB为一个指针,属于基本类型
	qDebug() << typeid(pB).name();
	// pB指向的B的对象,但是类B不存在virtual函数
	qDebug() << typeid(*pB).name();

	printTypeInfo(&a);
	printTypeInfo(&aa);

    return 0;
}

输出:

int
class CMM
int
class CMM
class CN * __ptr64
class CN
class CM const * __ptr64
class CM
class CM const * __ptr64
class CMM

        从输出的结果可以看出,无论printTypeInfo函数中指针pm指向的对象是基类CM的对象,还是指向派生类CMM的对象,typeid运行返回的pm的类型信息都是相同的,因为pm为一个静态类型,其类型名均为class CM const *__ptr64。但是typeid运算符却能正确地计算出了pm指向的对象的实际类型,分别为CM和CMM。

        那么typeid是如何推理出这个类型信息的呢?多态类的对象的类型信息保存在虚函数表的索引的-1的项中,该项是一个type_info对象的地址,该type_info对象保存着该对象对应的类型信息,每个类都对应着一个type_info对象,如下图所示:

C++中RTTI实现原理_第1张图片

3.dynamic_cast

        dynamic_cast运算符的作用是安全而有效地进行向下转型,而向下转型有潜在的危险性,因为基类的指针可以指向基类对象或其任何派生类的对象,而该对象并不一定是向下转型的类型的对象。
        把一个派生类的指针或引用转换成其基类的指针或引用总是安全的,因为通过分析对象的内存布局可以知道,派生类的对象中必然存在基类的子对象,所以通过基类的指针或引用对派生类对象进行的所有基类的操作都是合法和安全的。而向下转型有潜在的危险性,因为基类的指针可以指向基类对象或其任何派生类的对象,而该对象并不一定是向下转型的类型的对象。所以向下转型遏制了类型系统的作用,转换后对指针或引用的使用可能会引发错误的解释或腐蚀程序内存等错误。

C++中RTTI实现原理_第2张图片

      代码如下:

int main()
{
    CM* basePtr = new CMM();
    CMM* derivedPtr = dynamic_cast(basePtr);
    if (derivedPtr){
        qDebug() << "Dynamic cast successful.";
    }else{
        qDebug() << "Dynamic cast failed.";
    }
    delete basePtr;
    return 0;
}

        对于向下转换编译器则有真正的工作要做:例如对以下的继承链来说,将一个原本是C类型的对象的A*指针转换为C* 指针也很快,只需要检查一下type_info结构体是否相同,但如果要转换成其他类型则需要遍历树中A到指定类型的所有路径。

C++中RTTI实现原理_第3张图片

A* p = new C;
C* pC = dynamic_cast p;  // 成功,效率很高,仅一次比较
B* pB = dynamic_cast p;  // 成功,需要遍历树中A到C的路径,直到找到B
X* pX = dynamic_cast p;  // 成功,需要遍历树种A到C的路径,直到找到X
D* pD = dynamic_cast p;  // 失败,需要遍历树中A到C的路径,最后没找到,返回nullptr
P* pP = dynamic_cast p;  // 失败,P非多态类型

从上述的结果可以看出在向下转型中,只有dynamic_case才能实现安全的向下转型。那么dynamic_case是如何实现的呢?有了上面typeid和虚函数表的知识后,这个问题并不难解释了,以之前的转换为例。

1)计算指针或引用变量所指的对象的虚函数表的type_info信息,如下:

*(type_info*)pB->vptr[-1]

2)静态推导向下转型的目标类型的type_info信息,即获取类CMM的type_info信息

3)比较1)和2)中获取到的type_info信息,若2)中的类型信息与1)中的类型信息相等或是其基类类型,则返回相应的对象或子对象的地址,否则返回NULL。

引用的情况与指针稍有不同,失败时并不是返回NULL,而是抛出一个bad_cast异常,因为引用不能参考NULL。

4.缺陷

        由于 typeid 和 dynamic_cast 都是由编译器自己实现的, 所以性能没有统一的标准。同时,我们使用RTTI最多的场景可能是dynamic_cast来保证down cast的类型安全。但众所周知的是,dynamic_cast需要从虚表中查询类型信息,然后对比type_info,这个操作本身首先就很慢。
        dynamic_cast在很多情况下需要动态遍历继承树,并且一条条比对type_info中的类型元信息,在有的编译器中该比对被实现为字符串比较,效率更为低下。如果dynamic_cast使用得较多,则性能开销不小。
        此外,在使用上还存在一些问题:
a) 由于编译器可以开关默认的RTTI设置, 所以在多动态库使用场景中, 必须要求所有动态库都开启该选项。这就对外部人员进行二次开发有点强要求了。
b) 其次还会抛异常, 因此将不得不增加额外的异常处理逻辑。

5.一些库/软件提供的RTTI实现

5.1. CATIA的RTTI

如下是CATIA提供的RTTI实现,其中,虚方法IsA和IsAKindOf就是在运行时断言类型的。当明确类型时,就可以直接static_cast到特定的类型。其实可以发现,CATIA提供的实现类或者接口,都会通过CATDeclareClass或者CATDeclareInterface来进行声明。两个宏都会与CATMetaClass关联,在CATMetaClass中会有更多有关RTTI的实现细节。

//CATMacForIUnknown.h

#define CATDeclareClass                   \
\
private :                        \
    static CATMetaClass *meta_object;               \
public :                      \
    virtual CATMetaClass *  __stdcall GetMetaObject() const;    \
    virtual const char *              IsA() const;        \
    virtual int                       IsAKindOf(const char *) const;  \
    static CATMetaClass *   __stdcall MetaObject();       \
    static const CLSID &    __stdcall ClassId();          \
    static const char *     __stdcall ClassName();        \
    static CATBaseUnknown *CreateItself()

#define CATDeclareInterface                  \
\
private :                        \
    static CATMetaClass *meta_object;               \
public :                                                                \
    static CATMetaClass * __stdcall MetaObject();         \
    static const IID &    __stdcall ClassId();            \
    static const char *   __stdcall ClassName()

5.2. QT的RTTI

在Qt中,Q_OBJECT宏的使用会触发Qt元对象系统(QMetaObject System),从而实现了一种类似于RTTI的机制。下面是一个简单的例子,演示了如何在Qt中使用Q_OBJECT宏和元对象系统:

#include
#include 

class Animal : public QObject
{
    Q_OBJECT

public:

    Animal(QObject* parent = nullptr) : QObject(parent) {}
    virtual void makeSound() const 
    {
        qDebug() << "Generic animal sound";
    }
};

class Cat : public Animal
{
    Q_OBJECT

public:
    Cat(QObject* parent = nullptr) : Animal(parent) {}
    void makeSound() const override
    {
        qDebug() << "Meow!";
    }
};

class Dog : public Animal
{
    Q_OBJECT
public:
    Dog(QObject* parent = nullptr) : Animal(parent) {}
    void makeSound() const override
    {
        qDebug() << "Woof!";
    }
};

int main() 
{
    Animal* animalPtr = new Cat();

    // 使用 qobject_cast 进行动态类型转换
    Cat* catPtr = qobject_cast(animalPtr);
    if (catPtr)
    {
        qDebug() << "Successfully cast to Cat";
        catPtr->makeSound();  // 输出 "Meow!"
    }
    else 
    {
        qDebug() << "Failed to cast to Cat";
    }

    // 使用元对象信息输出类名
    const QMetaObject* metaObject = animalPtr->metaObject();
    qDebug() << "Object belongs to class:" << metaObject->className();

    delete animalPtr;
    return 0;
}

5.3. FreeCAD的RTTI

        FreeCAD中的RTTI系统也是通过宏来定义相关的虚函数与实现。如下DocumentObject类中声明的宏PROPERTY_HEADER_WITH_OVERRIDE,那么在CPP中会定义相关的实现,这时通过宏PROPERTY_SOURCE(DocumentObject, TransactionalObject)来实现。此外TYPESYSTEM_HEADER_WITH_OVERRIDE也是宏之一。

class AppExport DocumentObject: public TransactionalObject
{
    PROPERTY_HEADER_WITH_OVERRIDE(App::DocumentObject);

public:
    ...
    ...
    ...


#define PROPERTY_HEADER_WITH_OVERRIDE(_class_) \

TYPESYSTEM_HEADER_WITH_OVERRIDE(); \

protected: \

static const App::PropertyData * getPropertyDataPtr(void); \

virtual const App::PropertyData &getPropertyData(void) const override; \

private: \

static App::PropertyData propertyData


#define PROPERTY_SOURCE(_class_, _parentclass_) \

TYPESYSTEM_SOURCE_P(_class_)\

const App::PropertyData * _class_::getPropertyDataPtr(void){return &propertyData;} \

const App::PropertyData & _class_::getPropertyData(void) const{return propertyData;} \

App::PropertyData _class_::propertyData; \

void _class_::init(void){\

initSubclass(_class_::classTypeId, #_class_ , #_parentclass_, &(_class_::create) ); \

_class_::propertyData.parentPropertyData = _parentclass_::getPropertyDataPtr(); \

}


#define TYPESYSTEM_HEADER_WITH_OVERRIDE() \

public: \

static Base::Type getClassTypeId(void); \

virtual Base::Type getTypeId(void) const override; \

static void init(void);\

static void *create(void);\

private: \

static Base::Type classTypeId

6.实例

事实上,自定义实现RTTI可以提供更灵活和高效的解决方案。以下是一个简单的自定义实现RTTI的示例。主要是通过getName虚方法在运行时断言类型。

#include
#include 

class TypeInfo
{
public:
    virtual const char* getName() const = 0;
    virtual ~TypeInfo() {}
};

template 
class TypeID : public TypeInfo
{
public:
    const char* getName() const override
    {
        return typeid(T).name();
    }
};

class Base
{
public:
    virtual const TypeInfo& getType() const 
    {
        static TypeID type;
        return type;
    }
};

class Derived : public Base
{
public:
    const TypeInfo& getType() const override
    {
        static TypeID type;
        return type;
    }
};

int main() 
{
    Base* basePtr = new Derived();
    const TypeInfo& typeInfo = basePtr->getType();
    std::cout << "Type name: " << typeInfo.getName() << std::endl;
    if(typeInfo.getName() == "Derived") //进行类型识别
    {
        auto pDerived = static_cast(basePtr); //安全转换
    }
    delete basePtr;
    return 0;
}

7.总结

        C++的RTTI为程序员提供了在运行时获取类型信息的便利,但在某些情况下,特别是涉及性能要求高的应用中,开发者可能需要权衡使用默认RTTI机制的开销,并考虑是否需要自定义实现以满足特定需求。

        自定义实现RTTI可以提供更灵活和高效的类型信息管理方式。

        我们设计RTTI时,基本上是通过宏的方式载入一些虚函数或者类型来处理一个class,在运行时识别到具体类型,就可以通过static_cast来进行安全转换。

你可能感兴趣的:(#C++进阶,c++,开发语言)