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://www.mzph.cn/news/672984.shtml

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

相关文章

汇编笔记 01

小蒟蒻的汇编自学笔记&#xff0c;如有错误&#xff0c;望不吝赐教 文章目录 笔记编辑器&#xff0c;启动&#xff01;debug功能CS & IPmovaddsub汇编语言寄存器的英文全称中英对照表muldivandor 笔记 编辑器&#xff0c;启动&#xff01; 进入 debug 模式 debug功能 …

vue3-内置组件-KeepAlive

KeepAlive <KeepAlive> 是一个内置组件&#xff0c;它的功能是在多个组件间动态切换时缓存被移除的组件实例。 基本使用 默认情况下&#xff0c;一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态——当这个组件再一次被显示时&#xff0c;会创建…

【机器学习】机器学习流程之收集数据

&#x1f388;个人主页&#xff1a;甜美的江 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;机器学习 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步…

类与对象(终章)——友元,内部类,匿名对象

这里写目录标题 1. 友元1.2 友元函数1.3 友元类 2. 内部类3.匿名对象 1. 友元 之前实现日期类我们实现输入输出流重载的时候就已经了解了友元的概念&#xff0c;我们今天正式走进友元&#xff0c;详细地学习友元的各种特点与性质。 关键字:friend 1.2 友元函数 友元函数在重载…

vue3:26—新的内置组件

目录 Teleport Suspense Teleport 什么是Teleport? Teleport 是一种能够将我们的组件html结构移动到指定位置的技术 当在元素中的css使用了filter滤镜属性的时候&#xff0c;会导致内部 fixed 元素定位发生错误&#xff0c;即不再相对 viewport 进行定位&#xff0c;而是相对…

《山雨欲来-知道创宇 2023 年度 APT 威胁分析总结报告》

下载链接: https://pan.baidu.com/s/1eaIOyTk12d9mcuqDGzMYYQ?pwdzdcy 提取码: zdcy

Django模板(三)

一、标签URL 返回与给定视图和可选参数相匹配的绝对路径引用(不含域名的 URL) {% url some-url-name v1 v2 %} 第一个参数是url模式名称,后面跟着的是参数,以空格分隔可以使用关键字: {% url some-url-name arg1=v1 arg2=v2 %}如果您想检索命名空间的URL,请指定完全限定…

Msql-数据库死锁

实验案例 CREATE TABLE t1_deadlock ( id int(11) NOT NULL, name varchar(100) DEFAULT NULL, age int(11) NOT NULL, address varchar(255) DEFAULT NULL, PRIMARY KEY (id), KEY idx_age (age) USING BTREE, KEY idx_name (name) USING BTREE ) ENGINEInnoDB DEFAULT CHARS…

linux安装Webmin

简介 Webmin是功能最强大的基于Web的Unix系统管理工具。管理员通过浏览器访问Webmin的各种管理功能并完成相应的管理动作。Webmin支持绝大多数的Unix系统&#xff0c;这些系统除了各种版本的linux以外还包括&#xff1a;AIX、HPUX、Solaris、Unixware、Irix和FreeBSD等。 安装…

百面嵌入式专栏(面试题)内存管理相关面试题1.0

沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇我们将介绍内存管理相关面试题 。 一、内存管理相关面试题 page数据结构中的_refcount和_mapcount有什么区别?匿名页面和高速缓存页面有什么区别?page数据结构中有一个锁,我们称为页锁,请问trylock_page()和loc…

CAD-autolisp(四)——编译

目录 一、编译1.1 界面操作1.2 生成的应用程序&#xff08;二选一&#xff09; 二、后续学习 一、编译 编译&#xff1a;lsp后缀名为原文件&#xff0c;后缀名为fas、vlx为编译后文件&#xff0c;其会把sld、dcl、lsp等文件都编译进一个应用程序文件中加载&#xff1a;cad命令…

计算机二级C语言的注意事项及相应真题-2-程序修改

目录&#xff1a; 21.计算n!22.将单向链表结点(不包括头结点)数据域为偶数的值累加起来&#xff0c;并且作为函数值返回23.在字符串的最前端加入n个*号&#xff0c;形成新串&#xff0c;并且覆盖原串24.依次取出字符串中所有数字字符&#xff0c;形成新的字符串&#xff0c;并取…

删除.git的影响、git分支切换时注意事项

一、删除.git的影响 master分支文件 dev分支文件 删除.git后 文件为删除.git前分支的文件状态。 二、git分支切换时注意事项 情景&#xff1a;如果我在分支A&#xff0c;想要跳转到分支B。 git的规矩是&#xff0c;在那个分支上进行的提交&#xff0c;就算哪个分支上的工作…

白嫖10款游戏加速器,一年都不用开会员!

过年期间你们是走亲串戚还是窝家玩游戏、追剧&#xff1f;相信很多小伙伴都不会放过这个难得的假期&#xff0c;肯定是会百忙之中来两把的&#xff0c;那么人一多玩游戏肯定就会拥堵&#xff0c;有延迟。解决延迟最好的办法就是用加速器&#xff0c;当你的网络比别人强时&#…

浅谈交换原理(3)——交换网络

一、基本概念 交换网络是由若干个交换单元按照一定的拓扑结构和控制方式构成的网络。交换网络的三个基本要素是&#xff1a;交换单元、不同交换单元间的拓扑连接和控制方式。 1.1 单机交换网络与多级交换网络 交换网络按拓扑连接方式可分为&#xff1a;单级交换网络和多级交换网…

版本控制器Git

目录 背景 图形化界面 下载安装或使用网页版 安装图形化界面 注册账号 创建仓库​ 创建本地仓库 ​创建项目到本地工作目录 三板斧 git add git commit git push 注意 命令行 Git和Gitee/Github的区别&#xff1f; 版本控制器是什么&#xff1f; 本地仓库VS…

综合回溯,剪枝,暴搜

目录 力扣1863.找出所有子集的异或总和再求和 力扣47.全排列II​编辑 力扣17.电话号码的字母组合电话号码的字母组合https://leetcode.cn/problems/letter-combinations-of-a-phone-number/​编辑 力扣22.括号生成 力扣1863.找出所有子集的异或总和再求和 class Solution {in…

第三百一十三回

文章目录 1. 概念介绍2. 实现方法2.1 obscureText属性2.2 decoration属性 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何实现倒计时功能"相关的内容&#xff0c;本章回中将介绍如何实现密码输入框.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍…

GEE入门篇|栅格数据集概述(四):其他卫星产品

目录 1.甲烷数据集 2.天气及气候数据 3.预先分类的土地用途和土地覆盖数据集 3.1ESA WorldCover 3.2 全球森林变化数据集 卫星还可以收集有关气候、天气和大气中存在的各种化合物的信息。这些卫星利用部分电磁频谱&#xff0c;以及不同物体和化合物在不同波长的阳光照射下…

golang windows 环境搭建 环境配置

golang windows 环境搭建 环境配置 Golang学习之路一环境搭建 MacBook Linux 树莓派raspberrypi安装Golang环境 官网下载地址: https://go.dev/dl/ https://golang.google.cn/dl/ 下载对应系统版本&#xff0c;例如windows 64位系统&#xff0c;下载&#xff1a;xxx.window…