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 copiedvirtual ~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对象,如下图所示:

3.dynamic_cast

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

      代码如下:

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

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

A* p = new C;
C* pC = dynamic_cast<C*> p;  // 成功,效率很高,仅一次比较
B* pB = dynamic_cast<B*> p;  // 成功,需要遍历树中A到C的路径,直到找到B
X* pX = dynamic_cast<X*> p;  // 成功,需要遍历树种A到C的路径,直到找到X
D* pD = dynamic_cast<D*> p;  // 失败,需要遍历树中A到C的路径,最后没找到,返回nullptr
P* pP = dynamic_cast<P*> 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<QObject>
#include <QDebug>class Animal : public QObject
{Q_OBJECTpublic:Animal(QObject* parent = nullptr) : QObject(parent) {}virtual void makeSound() const {qDebug() << "Generic animal sound";}
};class Cat : public Animal
{Q_OBJECTpublic: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<Cat*>(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<iostream>
#include <string>class TypeInfo
{
public:virtual const char* getName() const = 0;virtual ~TypeInfo() {}
};template <typename T>
class TypeID : public TypeInfo
{
public:const char* getName() const override{return typeid(T).name();}
};class Base
{
public:virtual const TypeInfo& getType() const {static TypeID<Base> type;return type;}
};class Derived : public Base
{
public:const TypeInfo& getType() const override{static TypeID<Derived> 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<Derived*>(basePtr); //安全转换}delete basePtr;return 0;
}

7.总结

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/669521.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

信任与创新 | 回顾通付盾的2023!

-END- 数信云&#xff0c;基于区块链与人工智能的数据安全应用与服务平台

【Spring】Spring 启示录

一、OCP 开闭原则 核⼼&#xff1a;在扩展系统功能时不需要修改原先写好的代码&#xff0c;就是符合OCP原则的&#xff0c;反之修改了原先写好的代码&#xff0c;则违背了OCP原则的 若在扩展系统功能时修改原先稳定运⾏程序&#xff0c;原先的所有程序都需要进⾏重新测试&…

N-143基于springboot博客系统

开发工具&#xff1a;IDEA 服务器&#xff1a;Tomcat9.0&#xff0c; jdk1.8 项目构建&#xff1a;maven 数据库&#xff1a;mysql5.7 前端技术&#xff1a;AdminLTEHTML 服务端技术&#xff1a;springbootmybatis-plusthymeleaf 本项目分前台和后台&#xff0c;主要有普…

FM波的调制与解调

一、实验原理 1.FM的调制 产生调频信号有两种方法&#xff0c;直接调频法和间接调频法。间接调频法就是可以通过调相间接实现调频的方法。但电路较复杂&#xff0c;频移小&#xff0c;且寄生调幅较大&#xff0c;通常需多次倍频使频移增加。对调频器的基本要求是调频频移大&am…

大数据Doris(六十三):基于Doris的有道精品课数据中台建设实践

文章目录 基于Doris的有道精品课数据中台建设实践 一、背景

【Java程序设计】【C00245】基于Springboot的家政服务管理平台(有论文)

基于Springboot的家政服务管理平台&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的家政服务管理平台 本系统分为前台模块、管理员功能模块、用户功能模块以及服务人员功能模块。 前台模块&#xff1a;系统首页的…

Spring IOC 之深入分析 Aware 接口

&#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是小徐&#x1f947;☁️博客首页&#xff1a;CSDN主页小徐的博客&#x1f304;每日一句&#xff1a;好学而不勤非真好学者 &#x1f4dc; 欢迎大家关注&#xff01; ❤️ &#xfeff;AbstractAutowireCapableBeanFacto…

力扣分式化简

题目描述&#xff1a; 有一个同学在学习分式。他需要将一个连分数化成最简分数&#xff0c;你能帮助他吗&#xff1f; 连分数是形如上图的分式。在本题中&#xff0c;所有系数都是大于等于0的整数。 输入的cont代表连分数的系数&#xff08;cont[0]代表上图的a0&#xff0c;以…

Java学习-枚举类和泛型

1.枚举 什么是枚举类&#xff1f;格式是什么&#xff1f;&#xff1f; 枚举类的特点&#xff1a; 抽象枚举的注意点&#xff1a; 枚举的使用场景&#xff1a; 示例&#xff1a;枚举类对象作为参数传递 2.泛型 对泛型的认识&#xff1a; 自定义泛型类&#xff1a; 格式&#xff…

MySQL-运维-读写分离

一、介绍 二、一主一从读写分离 三、双主双从 1、介绍 2、准备 3、搭建 四、双主双从读写分离

“极简壁纸“爬虫JS逆向·实战

文章目录 声明目标分析确定目标目标检索 代码补全完整代码 爬虫逻辑完整代码 运行结果 声明 本教程只用于交流学习&#xff0c;不可用于商业用途&#xff0c;不可对目标网站进行破坏性请求&#xff0c;请遵守相关法律法规。 目标分析 确定目标 获取图片下载链接 目标检索…

OpenGL 入门(九)—Material(材质)和 光照贴图

文章目录 材质设置材质光的属性脚本实现 光照贴图漫反射贴图高光反射贴图 材质 材质本质是一个数据集&#xff0c;主要功能就是给渲染器提供数据和光照算法。 如果我们想要在OpenGL中模拟多种类型的物体&#xff0c;我们必须针对每种表面定义不同的材质(Material)属性。 我们…

【实训】自动运维ansible实训(网络管理与维护综合实训)

来自即将退役学长的分享&#xff0c;祝学弟学妹以后发大财&#xff01; 一 实训目的及意义 1.1 实训目的 1、熟悉自动化运维工具&#xff1a;实训旨在让学员熟悉 Ansible 这一自动化运维工具。通过实际操作&#xff0c;学员可以了解 Ansible 的基本概念、工作原理和使用方法…

Obsidian使用ddnsto穿透nas的webdav功能实现跨平台同步

之前一直用坚果云的webdav功能做obsidian的跨平台同步&#xff08;Windows&#xff0c;Ubuntu&#xff0c;iOS&#xff09;&#xff0c;但是今天在新的工作机上部署obsidian时&#xff0c;发现一次同步的文件数量超过了坚果云的限制&#xff08;付费用户好像是500次&#xff09…

springboot154基于Spring Boot智能无人仓库管理

简介 【毕设源码推荐 javaweb 项目】基于springbootvue 的 适用于计算机类毕业设计&#xff0c;课程设计参考与学习用途。仅供学习参考&#xff0c; 不得用于商业或者非法用途&#xff0c;否则&#xff0c;一切后果请用户自负。 看运行截图看 第五章 第四章 获取资料方式 **项…

2,cdc放缩位图

类似地&#xff0c;用pDC->StretchBlt来缩放&#xff0c;只是加上了两个参数&#xff0c;原始位图的宽高。 void CMy1_showbitmapView::StretchBitMap(CDC * pDC) { //CBitmap对象 CBitmap bitmap; //CDC对象 CDC dcMemory; //加载资源 bitmap.LoadBitmapW(IDB_BITMAP1); /…

基于单片机的智能寻光小车设计

摘 要&#xff1a;随着物联网技术的飞速发展和逐渐成熟&#xff0c;以单片机为主的智能小车在巡查、仓储、探险及国防等领域得到广泛应用。本文设计了一种基于单片机的智能寻光小车&#xff0c;该小车以STC89C52RC 芯片为设计核心&#xff0c;结合光敏传感器和超声波传感器等多…

leetcode(双指针)283.移动零(C++)DAY3

文章目录 1.题目示例提示 2.解答思路3.实现代码结果 4.总结 1.题目 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。 示例 示例 1: 输入…

浅析软件测试中的一些常见理论:杀虫剂效应、金字塔模型、缺陷集群性原则、软件测试活动依赖于软件测试背景、软件测试的7大基本原则

这篇文章我主要想记录学习一下在软件测试行业中的一些常见理论效应以做基本了解。 一、杀虫剂效应 1、杀虫剂效应介绍 杀虫剂效应原本指农业中随着农药的普及使用&#xff0c;害虫对农药的抗药性就越来越强&#xff0c;农药就越来越难杀死害虫。在农场里为了对付破坏农作物的…

《爬虫职海录》卷二 • 爬在广州

HI&#xff0c;朋友们好&#xff0c;「爬虫职海录」第二期更新啦&#xff01; 本栏目的内容方向会以爬虫相关的“岗位分析”和“职场访谈”为主&#xff0c;方便大家了解一下当下的市场行情。 本栏目持续更新&#xff0c;暂定收集国内主要城市的爬虫岗位相关招聘信息&#xf…