【问题标题】:Using typeid for simple decisions使用 typeid 进行简单决策
【发布时间】:2011-09-20 06:54:26
【问题描述】:

我的问题涉及在 C++ 运行时简单地捕获转换问题。我知道在大多数情况下 C++ 不提供“RTTI”(假设我不能更改我的编译器设置并将其打开,为了论证。)。

在我的项目中,我有一个古代图书馆提供给我的容器。让我们称其为 Frodo 和容器 Bane。

Frodo's Bane 是我必须直接与之交互的容器。它实施得很差,并且包含我必须单独管理的相同信息的多个地图。容器包含某个类型的多个实例,称为 Ring。

对于我的实现,我必须为所有其他操作使用 Ring 的子类型 OneRing。这是由于我对 Frodo's Bane 的特定实现的外部要求。

有时我必须查看我的清单,然后说我需要从 OneRing 中获得的东西,而我无法从任何旧 Ring 中获得。

有一个机会,只是偶尔的机会,佛罗多会将一个普通的旧戒指放入我的祸根中。

由于太过百事可乐,以至于没有意识到 C++ 无法回答诸如“此环是否也是 OneRing”之类的简单问题,或者无法捕捉因将 Ring 误认为是 OneRing 而产生的异常,所以我进行了一些谷歌搜索并发现有 typeid 和 type_of 之类的东西,但有很多理由不使用它们。

因此,将问题限制为只是想避免将 Ring 误认为是 OneRing,在不改变 Ring 的实现的情况下,有什么我可以安全有效地做的事情吗?

非常感谢您的任何建议!

更新:好的,下面有很多很好的建议,但经过一天的实验,我可以说以下关于原生 Visual Studio 2008 C++ 中 RTTI 的内容:

  • C++ 仅在 pointers 上具有 RTTI,指向 classes。您不能动态转换 void*,如果您询问类型信息,它将是 void*。 RTTI 在指针上。我理解您为什么不“强制转换”成员(从 C++ 的疯狂观点来看,在没有成员的语言中,您不必做出如此人为的区分),也许这些事实与某些过时的效率/编译器复杂性原因有关。
  • 如果您将 Ring* 的类型信息获取到 OneRing,它将是 Ring*。为什么他们选择不将信息放在 classes 我不知道。这使得 typeid() 基本上没有用 - 它甚至返回一个私有类,其中只有一个您可以访问的成员,一个 char* 名称!这就像不友好和无用等级的 11/10 - 他们是故意这样做的吗?
  • 可以使用动态转换安全地向下转换继承树,但同样您不能这样做,除非您拥有并且知​​道您收到的任何数据的共同继承祖先。
  • 如果您不属于上述情况,或者由于某种原因没有 RTTI,那么您将陷入困境,因为除非您实现,否则无法捕获异常或生成错误转换导致的错误您自己的某种形式的 RTTI。
  • COM 之类的东西试图通过为所有内容提供一个公共基础来提供更有用的 RTTI,该基础具有一些预先声明的 RTTI 信息...我认为。 :-)

附带说明,Microsoft 声明(至少自 2008 年以来)RTTI 是 C++ 的一部分,并且默认启用难以置信我发现了多少相互矛盾的信息。

对于那些大部分时间都在 Native Land 度过的人来说,这可能是个旧消息,但对我来说,这需要一段时间的实验、一些谷歌搜索、一些在 StackOverflow 上发布、更多谷歌搜索和更多实验。

感谢所有为我指明正确方向的人,如果您仍然觉得我遗漏了什么,请纠正我。 :-)

【问题讨论】:

  • TL;DR 版本 (IIUC):“我有一个容器,用于存储指向(?)某个基类的对象/指针,我想知道我得到的东西是否属于该基类它的派生类之一;我知道 RTTI 可能与我的问题有关,但出于某种原因我不喜欢它。”
  • 是的,干杯。作为记录,我审查并接受了一些答案。像这样的评论和答案是为什么我至今还没有接受任何不完整的答案讽刺我的问题的评论。似乎这里的许多人对听起来很酷而不是知识渊博更感兴趣。
  • @Derf:对上面的尖刻评论感到抱歉,但我认为整个 LoTR 事情只是分散了真正问题的注意力,这比第一次阅读你的问题时看起来要简单得多。 :)
  • @Matteo - 至少你读过我的帖子。当时这似乎是个好主意——你的总结有点让我的帖子看起来很长......并指出我忘了提到它是一个指针容器(只有几个 std::maps实际上)。哎呀! :-) FWIW 我认为我可能不得不在没有 RTTI 的情况下这样做的原因是因为老板不知道 RTTI 现在适用于我们所有的项目。 ;-) 我们现在至少应该有一些选择。
  • @Derf:顺便说一下,再看看我的帖子,我已经添加了对你的 cmets 的回复以及一些关于多态类和 RTTI 如何在 C++ 中工作的信息,我相信你就会明白为什么会出现这种行为。

标签: c++ casting rtti typeid


【解决方案1】:

如果容器包含Ring 的实例而不是指向它们的指针,则您不能拥有真正的OneRing,而只有一个放入Ring 的切片。在这种情况下,您无法确定对象在插入容器之前是什么,因为现在它是一个普通的Ring,用于各种实际用途。

如果容器存储指向元素的指针/引用(如我所想),您可以简单地尝试 dynamic_castRing 指针/引用类型 OneRing;如果dynamic_cast 成功,您的指针/引用正确地转换为它的“真实”类型OneRing,否则您知道它不是OneRing,而只是一个普通的Ring(或其他一些不是的派生类) t OneRing)。

显然dynamic_cast 需要 RTTI 才能正常工作(如果 RTTI 被禁用,则可执行文件中不会包含任何类型信息,因此在运行时对类型一无所知),所以如果您的编译器默认禁用它,请记住启用它。

如果无法开启,则无法使用dynamic_casttypeid;语言中没有内置“替代”机制,因为那将是多余的,并且需要 RTTI 所需的相同信息才能正常工作。

你唯一能做的就是在你的类层次结构中重新发明 RTTI,提供一个 virtual 方法被覆盖,以便每个子类返回不同的值;这样你就可以从Ring 指针测试它到底是什么Ring,然后你可以粗暴地施放它。请注意,这很麻烦,难以维护且不安全(残酷的指针转换在复杂的继承场景中可能不起作用),因此我强烈建议不要使用此方法。只需使用 RTTI。

顺便说一句,请记住,在很多情况下,如果您需要 dynamic_cast,那么您的类层次结构中就有问题:设计良好的类层次结构往往在没有动态转换的情况下通过正确使用 virtual 方法来实现其目标。

我的问题涉及在 C++ 运行时简单地捕获转换问题。我知道在大多数情况下 C++ 不提供“RTTI”(假设我不能更改我的编译器设置并将其打开,为了论证。)。

C++ 标准提供 RTTI,您可以在编译器中禁用它如果您不需要它,以节省一些可执行空间,这些空间会浪费在不必要的类型信息上。如果您需要 RTTI,则不清楚为什么要禁用它。

百事可乐一代还没有意识到 C++ 无法回答诸如“这枚戒指是否也是 OneRing”之类的简单问题,也无法捕捉因将 Ring 误认为是 OneRing 而引发的异常

这正是dynamic_cast 的目的。将其应用于指针,如果无法转换,则会得到 NULL,将其应用于引用,如果无法转换,则会抛出 std::bad_cast


附录

C++ 只有在指向类的指针上有 RTTI。您不能动态转换 void*,如果您询问类型信息,它将是 void*。 RTTI 在指针上。我理解您为什么不“强制转换”成员(从 C++ 的疯狂观点来看,在没有成员的语言中,您不必做出如此人为的区分),也许这些事实与某些过时的效率/编译器复杂性原因有关。

等等,等等,它不是这样工作的,RTTI 信息不在指针上,它以某种方式在对象上。但要理解为什么会如此混乱,我认为您需要了解一些关于这些东西如何在幕后工作的信息。

注意:以下所有内容都是特定于实现的,该标准没有规定任何特定的方式来实现虚函数和 RTTI,通常只是这样完成.

在 C++ 中,非多态类只是包含其字段的普通结构。私有/公共区别是在编译时强制执行的,这些方法只是普通函数,它们使用隐藏的this 参数调用,该参数指向调用它们的类的实例。每个方法调用都在编译时解决。继承只是在添加字段之前粘贴基类字段的问题。一切都很简单,每个人都很开心。

如果静态类型始终与“真实”对象类型相同,则一切正常。当您想要具有多态行为时就会出现问题:如果您将Derived * 存储到静态类型为Base * 的指针变量中,则“静态”类型和“真实”类型不再相同。当看到对此类指针执行的调用时,编译器不知道必须调用Derived 的方法,而只是调用Base 的方法,因为它拥有的所有信息都是此类的静态类型指针。

为了解决这个问题,发明了virtual 调用和vtables。当您将类方法声明为 virtual 时,您的类将成为多态类,即它允许声明为 virtual 的方法具有多态行为。

它是这样工作的。对于每个类,编译器都会创建一个函数指针表(所谓的“vtable”),将其放在可执行映像的某个位置;每个virtual 方法都有一个“行”。基类的 vtable 将包含指向虚拟方法的Base 实现的指针,而派生类将拥有它们的 vtable,其中包含指向其方法版本的指针。派生类的 vtables 也可能因为额外的方法而更大,但基类中也存在的方法在开头,Derived vtable 中的常用方法的索引与Base 的索引匹配。

现在,多态类的每个对象都有一个附加成员,即虚拟表指针(通常称为 vptr,通常放在类的开头)。该成员在每个构造函数运行以指向对象的正确 vtable 之前自动初始化。请注意,这就是为什么当在多态类型中运行基类构造函数时,虚方法不能“正确”工作(即它们的工作方式就像类是Base 类型一样):派生构造函数没有'还没有运行,所以 vptr 仍然指向 Base vtable。

当对象被完全构造后,它的“真实”类型现在比非多态对象更确定:如果你有一个Derived * 存储在Base * 中,编译器现在可以调用虚拟方法的正确版本:每个虚拟调用都实现为虚拟表中的查找(可以访问,因为 vptr 存在于 BaseDerived 广告相同的位置),然后调用该函数的指针存储在 vtable 的正确位置(特定虚函数的索引在每个派生类的 vtable 中都相同)。由于被调用函数“知道”它在 Derived 实例上运行,因此它可以访问 Derived 类的所有成员。

这或多或少是虚函数在“简单”多态类层次结构中为单一继承工作的方式。使用多重继承或将多态类与非多态类混合时,事情开始变得混乱(你可以有多个 vptr,类布局必须遵循一些约束,...),但这与我们无关。

现在我们可以使用虚函数了。 typeid 距离这里仅一步之遥。

RTTI 需要跟踪与每个对象的“真实”类型相关的一些信息。因此,由于我们已经有了 vtable,一个明智的想法是将这些信息(可能作为指向更大结构的指针)放在 vtable 的某个位置——例如在开头或结尾。这样,您不必在每个对象中添加另一个隐藏指针 - vtable 中的一个就足以用于虚拟调用和类型信息。

每个多态类都有一个包含其类型信息的关联结构,其中可能包括名称、一些唯一标识符(如果名称保证唯一,则可能不需要)以及可能指向基类的类型信息的指针(dynamic_cast 需要这些)。

当你在一个类上调用typeid 时,你会得到一个type_info 的实例,这是一个嵌入了一小部分信息的不透明类:即类名和隐藏的唯一标识符(实际上,通常它会只包含其中一个:如果编译器足够友好地提供一个名称,它可能会确保它是唯一的,以便将它也用作唯一标识符)。

name 成员对于非调试目的几乎没有用处。 type_info 的真正用处在于它提供的operator==;事实上,type_info 的主要目的是它可以比较,并且两个type_info 对象被评估为相等当且仅当它们是typeid 在相同类型上的结果。因此,typeid 的主要用途是检查对象的指针/引用的真实类型是否与另一个(或在编译时固定的类型)完全相同。比较通常是比较唯一标识符(可能是指向内存中的 RTTI 结构的指针)或唯一名称。

dynamic_cast,相反,是一个复杂得多的野兽。微不足道的情况(从派生类指针转换为基类指针)是微不足道的,不涉及运行时检查,并且它是唯一可以在非多态类(没有可用的 RTTI)上正常工作的情况.

第二种最简单的情况是当您尝试将“真实”类型为Derived *Base * 强制转换为Derived *。这只是将Derived * 的RTTI 指针与链接到要转换的对象的指针进行比较的问题;如果它们匹配,则转换成功。

相反,一般情况要复杂得多。 dynamic_cast 必须按照一些规则检查指向对象的类层次结构,以确保不执行无意义的强制转换(这些规则在 C++ 标准中指定,§5.2.7 ¶8)。使用上述类型信息执行此运行时检查。如果转换成功,你会得到一个正确转换的指针/引用,否则你会得到一个NULL(如果它是一个指针)或一个bad_cast 异常(如果它是一个引用)。

非多态类型呢?好吧,它们或多或少被 RTTI 遗漏了:typeid 将返回它们的静态类型信息(这通常是无用的),dynamic_cast 将只允许向上转换(可以在编译时检查),向下转换将在编译时失败-时间。

这个决定的基本原理可能是不要制作本应简单的“复杂”类型:使用隐藏指针将 RTTI 扩展到 每个 类型会使 POD 类型变得不可能(“Plain Old Data”,即类似于传统 C structs 的东西)。

另外,毕竟,如果你没有virtual 方法,你可能不需要dynamic_casttype_id:如果你有通过指向基类的指针来操作的对象层次结构,那么必须至少有一个 virtual 析构函数,否则可能会发生讨厌的事情(如果你 deleteDerived 通过 Base * 引用它并且析构函数不是virtual,你会只调用Base 的析构函数,而不是Derived 中定义的析构函数——绝对不好)。

现在您的其他 cmets 可以轻松回答:

如果您将 Ring* 的类型信息获取到 OneRing,它将是 Ring*。为什么他们选择不把这些信息放在我不知道的课程上。这使得 typeid() 基本上没有用 - 它甚至返回一个私有类,其中只有一个您可以访问的成员,一个 char* 名称!这就像不友好和无用等级的 11/10 - 他们是故意这样做的吗?

只有当Ring 不是多态时,你才会得到Ring *。您必须使Ring 的至少一个成员为多态,RTTI 才能工作(并且,如指定的那样,可能至少Ring 的析构函数必须是虚拟的)。 typeid 返回的类型的用处在于它可以与其他 type_info 进行比较 - name 成员仅用于调试目的(实际上,就标准而言,它总是可以返回一个空字符串)。

您可以使用动态转换安全地向下转换继承树,但同样您不能这样做,除非您拥有并且知​​道您收到的任何数据的共同继承祖先。

这是必需的,因为如果你有一个通用的void *,编译器就无法知道 vptr 在哪里 - 哎呀,它甚至不知道那个东西 是否有一个 vptr,它可能只是一个int *:-)

换句话说,编译器必须有一些最低限度的静态类型信息,以便能够判断在运行时是否有任何类型信息可用于执行决策(如果该类不是多态的,或者它甚至不是 class 强制转换在编译时失败)以及该信息在哪里(即 vptr 在对象中的位置 - 可以从任何祖先的静态类型推断出的信息)。

如果您不属于上述情况,或者由于某种原因没有 RTTI,那么您将陷入困境,因为除非您实现,否则无法捕获异常或生成由不正确的转换导致的错误自己的某种形式的 RTTI。

这就是语言的工作原理; RTTI 的其他实现选择(例如跟踪每个指针的类型)会更加复杂(如果可能的话)并且几乎没有额外的用处。如果您想要广泛的反射功能,C++ 不是最佳选择,您必须使用托管语言。


小例子总结:

#include <iostream>
#include <typeinfo>

using namespace std;

class Base_NoPolymorphic
{
public:
    void Name()
    {
        cout<<"Base_NoPolymorphic"<<endl;
    }
};

class Derived_NoPolymorphic : public Base_NoPolymorphic
{
public:
    void Name()
    {
        cout<<"Derived_NoPolymorphic"<<endl;
    }
};

class Base_Polymorphic
{
public:
    virtual void Name()
    {
        cout<<"Base_Polymorphic"<<endl;
    }
};

class Derived_Polymorphic : public Base_Polymorphic
{
public:
    virtual void Name()
    {
        cout<<"Derived_Polymorphic"<<endl;
    }
};

int main()
{
    Base_NoPolymorphic bnp;
    Derived_NoPolymorphic dnp;
    Base_Polymorphic bp;
    Derived_Polymorphic dp;

    // Non-polymorphic stuff: nothing works as expected
    Base_NoPolymorphic *bnpp=&bnp, *dnpp=&dnp;
    // These will always behave as Base_NoPolymorphic
    cout<<typeid(bnpp).name()<<" - ";
    bnpp->Name();
    cout<<typeid(dnpp).name()<<" - ";
    dnpp->Name();
    // This does not compile, because you're trying a cast down the class hierachy
    // on a non-polymorphic type
    // cout<<dynamic_cast<Derived_NoPolymorphic *>(dnpp)<<endl;

    //Polymorphic stuff: everything works fine
    Base_Polymorphic *bpp=&bp, *dpp=&dp;
    // These will behave correctly depending on their "real" type
    cout<<typeid(bpp).name()<<" - ";
    bpp->Name();
    cout<<typeid(dpp).name()<<" - ";
    dpp->Name();
    // This will succeed (output != 0)
    cout<<dynamic_cast<Derived_Polymorphic *>(dpp)<<endl;

    // Notice that also with polymorphic stuff a void * remains a black box
    void * ptr=&dp;
    cout<<typeid(ptr).name()<<endl;
    // This does not compile, because the source static type is a non-class type
    // cout<<dynamic_cast<Derived_Polymorphic *>(ptr)<<endl;
    return 0;
}

在我的机器上输出 (g++ 4.5):

P18Base_NoPolymorphic - Base_NoPolymorphic
P18Base_NoPolymorphic - Base_NoPolymorphic
P16Base_Polymorphic - Base_Polymorphic
P16Base_Polymorphic - Derived_Polymorphic
0x7fffd7ad6890
Pv

正如预期的那样:非多态类型上的typeid 仅返回静态类型信息(Pvvoid * 的 g++ RTTI“名称”),而在多态类型上工作正常;通过基类指针调用virtual/nonvirtual 方法也是如此。 dynamic_cast 在多态类型上成功,如果你取消注释另一个 dynamic_cast,你会看到它编译失败。

【讨论】:

  • 我的印象是 dynamic_cast 不能保证向下转换层次结构,只能向上转换 (cplusplus.com/doc/tutorial/typecasting)。我还认为 RTTI 在大多数环境中默认是禁用的 (stackoverflow.com/questions/1986418/typeid-and-typeof-in-c)。虽然我必须检查您的建议是否适用于我的情况,但我会接受您的回答。
  • @Derf:是的,我忘了那个; dynamic_cast 只有在类层次结构是多态的(即至少有一个虚拟方法)时才能正确构建层次结构,否则 RTTI 不会启动。关于第二个,IIRC RTTI 默认使用 g++,并且它在旧版 VC++ 版本中默认关闭(但我不知道 VC++ >7.1 的默认设置)。在任何一种情况下,请记住启用/禁用它只是在编译器命令行上添加/删除开关或在项目属性中标记一个框。所以,没什么大不了的。 :)
【解决方案2】:

我了解 C++ 不提供 'RTTI' 在大多数情况下(让我们 只是说我不能改变我的编译器 设置并将其打开,对于 争论的缘故。)。

那是错误的。 C++ 提供了typeid()dynamic_cast,如果你的编译器没有提供它或者你没有打开它,那是你的问题。

我用谷歌搜索了一下,发现 有 typeid 和 type_of,但是有很多 不使用每个的原因。

type_of 是 C++/CLI,而不是 C++,但 typeid() 是非常好的 C++,任何告诉你不要使用它的人都是白痴,仅此而已。有些 1980 年代的狂热者会告诉你不要将它用于性能或二进制膨胀或其他 30 年前可能相关的废话,但在本世纪编写的 C++ 中,没有任何借口或理由不使用typeid().

您也可以使用dynamic_cast,这正是该语言的用途。

【讨论】:

  • 第一点,如果我使用的是没有 RTTI 构建的库,这也是我的问题,不是吗?其次,我真诚地希望您对 typeid 没问题的看法是正确的。最后,请参阅我对上一个关于 dynamic_cast 的答案的评论。
  • 其实,对于typeid,大家常说的一句话是,当dynamic_cast更合适时,人们往往会使用它。
猜你喜欢
  • 2016-01-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-12
  • 2020-04-07
  • 1970-01-01
  • 2017-04-12
  • 2015-06-16
相关资源
最近更新 更多