C++之RTTI实现原理

相关系列文章

C++无锁队列的原理与实现

如何写出高质量的函数?快来学习这些coding技巧

从C++容器中获取存储数据的类型

C++之多层 if-else-if 结构优化(一)

C++之多层 if-else-if 结构优化(二)

C++之多层 if-else-if 结构优化(三)

C++之Pimpl惯用法

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的对象。因为其只声明了一个构造函数(复制构造函数)且为delete,所以编译器不会合成任何的构造函数,而且赋值操作运行符也为delete。这两个操作就完全禁止了用户对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和CMM都是类型名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对象,如下图所示:

9432f5b0dd3143aeb2d6756ef3010896.png

3.dynamic_cast

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

296ef5e17eb94adbb9e5ec019d7de223.png

      代码如下:

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到指定类型的所有路径。

6e2f0d3481e3419aa1237a300411fef2.png

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_cast才能实现安全的向下转型。那么dynamic_cast是如何实现的呢?有了上面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://xiahunao.cn/news/2777391.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Swift Combine 使用 dataTaskPublisher 发起网络请求 从入门到精通十

Combine 系列 Swift Combine 从入门到精通一Swift Combine 发布者订阅者操作者 从入门到精通二Swift Combine 管道 从入门到精通三Swift Combine 发布者publisher的生命周期 从入门到精通四Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五Swift Com…

旅游|基于Springboot的旅游管理系统设计与实现(源码+数据库+文档)

旅游管理系统目录 目录 基于Springboot的旅游管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、用户管理 2、景点分类管理 3、景点信息管理 4、酒店信息管理 5、景点信息 6、游记分享管理 四、数据库设计 1、实体ER图 2、具体的表设计如下所示&#xf…

【从Python基础到深度学习】4. Linux 常用命令

1.配置root用户密码 root用户为系统默认最高权限用户&#xff0c;其他用户密码修改命令与root用户修改密码命令相同 sudo passwd root 2.添加用户&#xff08;henry&#xff09; sudo useradd -m henry -s /bin/bash 3.配置henry用户密码 Xshell下连接新用户&#xff08;hen…

小巨人大爆发:紧凑型大型语言模型效率之谜揭晓!

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

图像处理常用算法—6个算子 !!

目录 前言 1、Sobel 算子 2、Isotropic Sobel 算子 3、Roberts 算子 4、Prewitt 算子 5、Laplacian算子 6、Canny算子 前言 同图像灰度不同&#xff0c;边界处一般会有明显的边缘&#xff0c;利用此特征可以分割图像。 需要说明的是&#xff1a;边缘和物体间的边界并不…

Django问题报错:TypeError: as_view() takes 1 positional argument but 2 were given

一、错误位置 from django.urls import pathfrom users_app.views import RegisterView, LoginView, LogoutViewapp_name users urlpatterns [path("register/", RegisterView.as_view, name"register"),path("login/", LoginView.as_view, n…

机器学习---学习与推断,近似推断、话题模型

1. 学习与推断 基于概率图模型定义的分布&#xff0c;能对目标变量的边际分布&#xff08;marginal distribution&#xff09;或某些可观测变量 为条件的条件分布进行推断。对概率图模型&#xff0c;还需确定具体分布的参数&#xff0c;称为参数估计或学习问 题&#xff0c;…

读千脑智能笔记08_人工智能的未来(下)

1. 机器智能存在的风险 1.1. “人工智能”这个名字应用到几乎所有涉及机器学习的领域 1.2. 技术专家对人工智能的态度也从“人工智能可能永远不会实现”快速转变为“人工智能可能在不久的将来毁灭所有人类” 1.3. 每一项新技术都可能会被滥用…

专业课135+总分400+西安交通大学815/909信号与系统考研电子信息与通信工程,真题,大纲,参考书。

经过将近一年的考研复习&#xff0c;终于梦圆西安交大&#xff0c;今年专业可815(和909差不多)信号与系统135&#xff0c;总分400&#xff0c;回想这一年的复习还是有很多经验和大家分享&#xff0c;希望可以对大家复习有所帮助&#xff0c;少走弯路。 专业课&#xff1a; 这…

18:蜂鸣器

蜂鸣器 1、蜂鸣器的介绍2、编程让蜂鸣器响起来3、通过定时控制蜂鸣器4、蜂鸣器发出滴滴声&#xff08;间歇性鸣叫&#xff09; 1、蜂鸣器的介绍 蜂鸣器内部其实是2个金属片&#xff0c;当一个金属片接正电&#xff0c;一个金属片接负电时&#xff0c;2个金属片将合拢&#xff…

大数据应用对企业的价值

目录 一、大数据应用价值 1.1 大数据技术分析 1.2 原有技术场景的优化 1.2.1 数据分析优化 1.2.2 高并发数据处理 1.3 通过大数据构建新需求 1.3.1 智能推荐 1.3.2 广告系统 1.3.3 产品/流程优化 1.3.4 异常检测 1.3.5 智能管理 1.3.6 人工智能和机器学习 二、大数…

【深度学习: ChatGPT 】经验教训:使用 ChatGPT 作为 ML 工程师一天

【深度学习&#xff1a; ChatGPT 】经验教训&#xff1a;使用 ChatGPT 作为 ML 工程师一天 介绍设置过程标杆ChatGPT 做机器学习ChatGPT 能否真正实施这些解决方案&#xff1f;结果结论 TLDR;在最近使用 AI 应用程序 ChatGPT 的用例激增中&#xff0c;我们询问它是否可用于改进…

肯尼斯·里科《C和指针》第12章 使用结构和指针(1)链表

只恨当时学的时候没有读到这本书&#xff0c;&#xff0c;&#xff0c;&#xff0c;&#xff0c;&#xff0c; 12.1 链表 有些读者可能还不熟悉链表&#xff0c;这里对它作一简单介绍。链表(linked list)就一些包含数据的独立数据结构&#xff08;通常称为节点&#xff09;的集…

【数学建模】【2024年】【第40届】【MCM/ICM】【A题 七鳃鳗性别比与资源可用性】【解题思路】

我们通过将近半天的搜索数据&#xff0c;查到了美国五大湖中优势物种的食物网数据&#xff0c;以Eric伊利湖为例&#xff0c;共包含34各优势物种&#xff0c;相互之间的关系如下图所示&#xff1a; 一、题目 &#xff08;一&#xff09; 赛题原文 2024 MCM Problem A: Reso…

704. Binary Search(二分查找)

题目描述 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 问题分析 确定左右界&#xff0c;然后按规则进行更新即可 代…

H12-821_73

73.某台路由器Router LSA如图所示&#xff0c;下列说法中错误的是&#xff1f; A.本路由器的Router ID为10.0.12.1 B.本路由器为DR C.本路由器已建立邻接关系 D.本路由器支持外部路由引入 答案&#xff1a;B 注释&#xff1a; LSA中的链路信息Link ID&#xff0c;Data&#xf…

Linux探秘:如何用 find 命令发现隐藏的宝藏

&#x1f31f;&#x1f30c; 欢迎来到知识与创意的殿堂 — 远见阁小民的世界&#xff01;&#x1f680; &#x1f31f;&#x1f9ed; 在这里&#xff0c;我们一起探索技术的奥秘&#xff0c;一起在知识的海洋中遨游。 &#x1f31f;&#x1f9ed; 在这里&#xff0c;每个错误都…

python 爬虫篇(3)---->Beautiful Soup 网页解析库的使用(包含实例代码)

Beautiful Soup 网页解析库的使用 文章目录 Beautiful Soup 网页解析库的使用前言一、安装Beautiful Soup 和 lxml二、Beautiful Soup基本使用方法标签选择器1 .string --获取文本内容2 .name --获取标签本身名称3 .attrs[] --通过属性拿属性的值标准选择器find_all( name , at…

动漫风博客介绍页面源码

动漫风博客介绍页面源码&#xff0c;HTML源码&#xff0c;图片背景有淡入切换特效 蓝奏云&#xff1a;https://wfr.lanzout.com/iIDZu1nrmjve

Web前端框架-Vue(初识)

文章目录 web前端三大主流框架**1.Angular****2.React****3.Vue**什么是Vue.js 为什么要学习流行框架框架和库和插件的区别一.简介指令v-cloakv-textv-htmlv-pre**v-once**v-onv-on事件函数中传入参数事件修饰符双向数据绑定v-model 按键修饰符自定义按键修饰符别名v-bind(属性…