为什么“纯多态”优于使用RTTI?


106

我所见过的几乎所有讨论这种事情的C ++资源都告诉我,与使用RTTI(运行时类型标识)相比,我应该更喜欢多态方法。总的来说,我会认真对待这种建议,并会尝试并理解其基本原理-毕竟,C ++是一头强大的野兽,很难全面理解。但是,对于这个特定的问题,我正在空白,想看看互联网可以提供什么样的建议。首先,让我总结一下到目前为止我所学到的东西,列出引起RTTI被“认为有害”的常见原因:

一些编译器不使用它/ RTTI并非始终启用

我真的不赞成这种说法。这就像说我不应该使用C ++ 14功能,因为那里有不支持它的编译器。但是,没有人会阻止我使用C ++ 14功能。大多数项目将对他们正在使用的编译器及其配置方式产生影响。甚至引用了gcc手册页:

-fno-rtti

使用C ++运行时类型标识功能(dynamic_cast和typeid)禁止使用虚拟函数生成有关每个类的信息。如果您不使用语言的这些部分,则可以使用此标志节省一些空间。请注意,异常处理使用相同的信息,但是G ++会根据需要生成它。dynamic_cast运算符仍可用于不需要运行时类型信息的强制转换,即强制转换为“ void *”或明确的基类。

这说明如果我不使用RTTI,可以将其禁用。这就像在说,如果您不使用Boost,则不必链接到它。我不需要为有人正在编译的情况做任何计划-fno-rtti。另外,在这种情况下,编译器将大声失败。

花费额外的内存/可能很慢

每当我想使用RTTI时,这意味着我需要访问类的某种类型信息或特征。如果我实现了不使用RTTI的解决方案,这通常意味着我将不得不在类中添加一些字段来存储此信息,因此memory参数实在是无效的(我会在下面进一步举例说明)。

实际上,dynamic_cast可能很慢。不过,通常有避免使用速度要求严格的情况的方法。而且我还没有看到替代方案。这样的SO答案建议使用基类中定义的枚举来存储类型。这只有在您知道所有派生类都是先验的情况下才有效。相当大的“如果”!

从这个答案来看,RTTI的成本似乎也不清楚。不同的人测量不同的东西。

优雅的多态设计将使RTTI不再必要

这是我认真对待的建议。在这种情况下,我简直无法提出涵盖我的RTTI用例的良好的非RTTI解决方案。让我举一个例子:

假设我正在编写一个库来处理某种对象的图形。我想允许用户在使用我的库时生成自己的类型(因此enum方法不可用)。我的节点有一个基类:

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
};

现在,我的节点可以是不同的类型。这些怎么样:

class red_node : virtual public node_base
{
  public:
    red_node();
    virtual ~red_node();

    void get_redness();
};

class yellow_node : virtual public node_base
{
  public:
    yellow_node();
    virtual ~yellow_node();

    void set_yellowness(int);
};

地狱,为什么不选择其中之一:

class orange_node : public red_node, public yellow_node
{
  public:
    orange_node();
    virtual ~orange_node();

    void poke();
    void poke_adjacent_oranges();
};

最后一个功能很有趣。这是一种编写方法:

void orange_node::poke_adjacent_oranges()
{
    auto adj_nodes = get_adjacent_nodes();
    foreach(auto node, adj_nodes) {
        // In this case, typeid() and static_cast might be faster
        std::shared_ptr<orange_node> o_node = dynamic_cast<orange_node>(node);
        if (o_node) {
             o_node->poke();
        }
    }
}

这一切看起来都很清晰。我不必在不需要它们的地方定义属性或方法,基节点类可以保持精简和有意义。没有RTTI,我应该从哪里开始?也许我可以在基类中添加一个node_type属性:

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();

  private:
    std::string my_type;
};

std :: string是类型的好主意吗?也许不是,但是我还能使用什么呢?编一个号码,希望没有人在使用它吗?另外,对于我的orange_node,如果我想使用red_node和yellow_node中的方法怎么办?每个节点是否必须存储多种类型?这看起来很复杂。

结论

这个示例似乎并不太复杂或异常(我在日常工作中使用的是类似的东西,其中节点表示通过软件控制的实际硬件,并且根据它们的不同而做不同的事情)。但是我不知道使用模板或其他方法来执行此操作的干净方法。请注意,我试图理解问题,而不是为我的例子辩护。我阅读了上面链接的SO答案等页面以及Wikibooks上的该页面似乎表明我滥用了RTTI,但我想了解原因。

那么,回到我最初的问题:为什么“纯多态性”优于使用RTTI?


9
解决“戳橘子”示例的“缺失”(作为语言功能)将是多重调度(“多重方法”)。因此,寻找模仿的方法可能是另一种选择。因此,通常使用访客模式。
Daniel Jour

1
使用字符串作为类型不是很有帮助。使用指向某个“类型”类的实例的指针将使此过程更快。但是然后您基本上是在手动执行RTTI会执行的操作。
Daniel Jour

4
@MargaretBloom不,不是,RTTI代表运行时类型信息,而CRTP仅代表模板-静态类型。
edmz

2
@ mbr0wn:所有工程过程都受某些规则约束;编程也不例外。规则可以分为两个存储段:规则(SHOULD)和规则(MUST)。(可以这么说,还有一个“建议/选项桶”(COULD)。)阅读C / C ++标准(实际上是任何其他英语标准)是如何定义它们的。我猜您的问题出在您误以为“不使用RTTI”作为硬性规定(“您不使用RTTI”)。这实际上是一条软规则(“您不应使用RTTI”),这意味着您应尽可能避免使用它-并在无法避免的情况下使用

3
我注意到很多答案并没有注意到您的示例建议的想法node_base是库的一部分,用户将创建自己的节点类型。然后他们将无法修改node_base以允许其他解决方案,因此RTTI可能会成为他们的最佳选择。另一方面,还有其他方法可以设计这样的库,从而无需使用RTTI即可使新节点类型更加优雅地适应(也可以使用其他方式来设计新节点类型)。
马修沃尔顿

Answers:


69

接口描述了在给定情况下进行交互时需要知道的内容。一旦使用“您的整个类型层次结构”扩展了接口,您的接口“表面积”就会变得很大,这使得进行推理变得更加困难

例如,您“戳相邻的橙子”意味着我作为第三方不能模仿橙子!您私下声明了一个橙色类型,然后使用RTTI使您的代码在与该类型进行交互时表现得特别。如果我想“变成橙色”,我必须在您的私家花园内。

现在,每个带有“橙色”的人都将与您的整个橙色类型以及隐式的整个私人花园(而不是已定义的界面)结合。

乍一看,这看起来像是扩展受限接口而不必更改所有客户端(添加am_I_orange)的好方法,而趋向于发生的是它使代码库僵化了,并阻止了进一步的扩展。特殊的橙色变成系统功能所固有的,并阻止您创建以不同方式实现的橙色的“橘子”替代品,并可能消除依赖关系或优雅地解决其他一些问题。

这确实意味着您的界面必须足以解决您的问题。从这个角度来看,为什么只需要戳桔子,如果是,为什么界面中不存在桔子?如果需要一些可以临时添加的模糊标签,可以将其添加到您的类型中:

class node_base {
  public:
    bool has_tag(tag_name);

这将使您的界面从狭义指定到广泛的基于标签的大规模扩展。除了不是通过RTTI和实现细节(即,“如何实现?使用橙色类型?可以通过。”)来完成此操作之外,它通过完全不同的实现轻松模拟。

如果需要,它甚至可以扩展到动态方法。“你支持巴兹,汤姆和爱丽丝的争论吗?好吧,你。” 在一个大的意义,这是侵入比动态转换为获得在事实的另一个对象是你知道的类型。

现在,橘子对象可以带有橙色标记并随其执行,同时实现分离。

它仍然可能导致巨大的混乱,但是至少是消息和数据的混乱,而不是实现层次结构。

抽象是一种解耦和隐藏不确定性的游戏。它使代码更容易在本地进行推理。RTTI在整个抽象到实现细节方面无聊。这可以使解决问题变得更加容易,但是却要付出代价,将您真正锁定在一个特定的实现中非常容易。


14
最后一段+1;不仅是因为我同意你的观点,而且还因为这是钉子。

7
一旦知道对象被标记为支持该功能,如何获得特定功能?这要么涉及强制转换,要么具有神级的成员功能。第一种可能性是未检查的强制转换(在这种情况下,标记只是自己的非常容易出错的动态类型检查方案),或者是已检查的dynamic_cast(RTTI),在这种情况下,标记是多余的。第二种可能性,上帝的阶级,是可憎的。总结起来,这个答案有很多词,我认为对Java程序员来说听起来不错,但实际内容却毫无意义。
干杯和健康。-Alf

2
@Falco:这是我提到的第一种可能性(的一种变体),即基于标记的未经检查的转换。在这里标记是自己的非常脆弱且非常容易出错的动态类型检查方案。任何小的客户端代码行为都不正常,在C ++中,UB-land处于关闭状态。您不会像在Java中那样遇到异常,但是会遇到未定义的行为,例如崩溃和/或错误的结果。与更加理智的C ++代码相比,除了极其不可靠和危险之外,效率也非常低下。哎呀,这是非常非常不好的。非常如此。
干杯和健康。-Alf

1
嗯 :)参数类型?
干杯和健康。-Alf

2
@JojOatXGME:因为“多态”意味着能够使用多种类型。如果您必须检查它是否是特定类型,则除了用于获取指针/引用的现有类型检查之外,还需要寻找多态性。您使用的不是多种类型;您正在使用特定类型。是的,有“(大型)Java项目”可以做到这一点。但这就是Java;该语言仅允许动态多态。C ++也具有静态多态性。而且,仅仅因为某人“大”就做,这并不是一个好主意。
Nicol Bolas's

31

对该功能或那个功能的大多数道德劝说是典型性,其源于观察到对该功能的使用存在许多误解

道德失败是他们假定所有的用途都误解,而其实是有原因的存在特点。

他们有我以前所说的“水管工综合体”:他们认为所有的水龙头都出现故障,因为他们要维修的所有水龙头都是。现实情况是,大多数水龙头工作得很好:您根本不称呼他们为水管工!

可能发生的疯狂事情是,为了避免使用给定功能,程序员编写了许多样板代码实际上实际上是在私下重新实现该功能。(您是否遇到过既不使用RTTI也不进行虚拟调用的类,却具有跟踪它们实际的派生类型的值?这不过是伪装成RTTI而已。)

有一种通用的方式来考虑多态性:IF(selection) CALL(something) WITH(parameters)。(对不起,但是编程,而无视抽象,仅此而已)

设计时(概念),编译时(基于模板推导),运行时(基于继承和基于虚拟函数)或数据驱动(RTTI和切换)多态的使用取决于已知的决策数量。在每个的阶段的生产,以及如何,他们都在每一个方面。

这个想法是:

您可以预期的越多,捕获错误并避免错误影响最终用户的机会就越大。

如果所有内容(包括数据)都是恒定的,则可以使用模板元编程来完成所有操作。在对实现的常量进行编译之后,整个程序可以归结为一个只返回结果的return语句。

如果在编译时有很多已知的情况,但您不知道它们必须作用的实际数据,则可以使用编译时多态(主要是CRTP或类似的方法)。

如果案例的选择取决于数据(不是编译时已知的值),并且切换是一维的(只能将操作减少为一个值),则基于虚拟函数的调度(或通常的“函数指针表”) ”)。

如果切换是多维的,则由于C ++中不存在本机多个运行时分派,因此您必须:

  • 通过Goedelization缩减为一个维度:这是虚拟基础和多重继承的地方,带有菱形堆积的平行四边形,但这需要已知的可能组合的数量并且必须相对较小。
  • 将维度彼此链接(就像在“复合访客”模式中那样,但这要求所有类都意识到他们的其他同级,因此它不能从构思的地方“缩小”)
  • 根据多个值调度呼叫。这正是RTTI的目的。

如果不仅是切换,而且甚至不知道动作的编译时间,那么也需要脚本和解析:数据本身必须描述要对其执行的动作。

现在,由于我所列举的每种情况都可以看作是其后发生的特殊情况,因此您可以通过滥用最底层的解决方案来解决所有问题,也可以针对最顶层的可承受的问题进行滥用。

这就是道德化实际上要避免的东西。但这并不意味着不存在最底层的问题!

扑打RTTI只是为了扑打它,就像扑打只是为了扑打它一样goto。鹦鹉而不是程序员的东西。


很好地说明每种方法适用的级别。不过,我还没有听说过“ Goedelization”-它是否也以其他名称闻名?您能否添加链接或更多说明?谢谢:)
j_random_hacker

1
@j_random_hacker:我也对使用Godelization感到好奇。通常,人们首先将Godelization视为从一个字符串映射到某个整数,然后再使用此技术以形式语言生成自引用语句。我不熟悉虚拟派遣中的这个术语,并且希望了解更多信息。
埃里克·利珀特

1
实际上,我在滥用该术语:根据Goedle的说法,由于每个整数都对应一个整数n-ple(其素数的幂),每个n-ple都对应一个整数,因此每个离散n维索引问题都可以简化为一维的。这并不意味着这是唯一的方法:这只是说“有可能”的一种方法。您需要的只是“分而治之”的机制。虚拟函数是“划分”,多重继承是“征服”。
Emilio Garavaglia '16

...当所有在有限域(范围)内发生的事件时,线性组合会更有效(经典i = r * C + c获取矩阵单元格数组中的索引)。在这种情况下,分隔ID为“访问者”,而征服者为“复合”。由于涉及线性代数,因此在这种情况下,该技术对应于“对角化”
Emilio Garavaglia

别以为这些都是技巧。它们只是类推
Emilio Garavaglia '16

23

在一个小例子中,它看起来很整洁,但是在现实生活中,您很快就会遇到一整套可以互相戳破的类型,其中一些类型可能只朝一个方向。

怎么样dark_orange_node,或者black_and_orange_striped_node,还是dotted_node?可以有不同颜色的点吗?如果大多数圆点都是橙色怎么办?

并且每次您必须添加新规则时,都将不得不重新访问所有poke_adjacent功能并添加更多的if语句。


与往常一样,创建通用示例很困难,我将为您提供帮助。

但是,如果要执行此特定示例,则将一个poke()成员添加到所有类中,void poke() {}如果它们不感兴趣,则让其中一些忽略调用()。

当然,这将比比较typeids 还要便宜。


3
您说“肯定”,但是什么使您如此确定?那就是我要设法弄清楚的。假设我将orange_node重命名为pokable_node,它们是我唯一可以调用poke()的对象。这意味着我的接口将需要实现一个poke()方法,该方法抛出一个异常(“此节点不可放置”)。这似乎昂贵。
mbr0wn

2
他为什么需要抛出异常?如果您关心接口是否为“可戳”的,则只需添加一个函数“ isPokeable”,然后在调用poke函数之前先对其进行调用。或者只是按照他所说的去做,“在不可oke的课堂上什么也不做”。
布兰登

1
@ mbr0wn:更好的问题是为什么您希望可pokable和不可pokable节点共享相同的基类。
Nicol Bolas's

2
@NicolBolas为什么要让友好和敌对的怪物共享相同的基类,或者可聚焦和不可聚焦的UI元素,或者使用数字键盘的键盘和没有数字键盘的键盘?
user253751'3

1
@ mbr0wn这听起来像行为模式。底座接口有两个方法supportsBehaviourinvokeBehaviour每个类可以具有一个行为列表。一种行为是Poke,并且所有希望被戳的类都可以将其添加到“受支持的行为”列表中。
Falco

20

一些编译器不使用它/ RTTI并非始终启用

我相信您误解了这些论点。

在许多C ++编码场所中,不使用RTTI。使用编译器开关强制禁用RTTI的位置。如果您是在这种范式中进行编码的...那么,您几乎可以肯定已经知道了这种限制。

因此,问题出在图书馆。也就是说,如果您正在编写依赖于RTTI的库,那么关闭RTTI的用户将无法使用您的库。如果您希望这些人使用您的图书馆,那么即使您的图书馆也被可以使用RTTI的人使用,它也不能使用RTTI。同样重要的是,如果您不能使用RTTI,则必须多花些钱购买图书馆,因为RTTI的使用对您来说很重要。

花费额外的内存/可能很慢

您在热循环中没有做很多事情。您不分配内存。您无需遍历链表。依此类推。RTTI当然可以成为那些“不要在这里做”的事情中的另一个。

但是,请考虑所有RTTI示例。在所有情况下,您都有一个或多个不确定类型的对象,并且您想对它们执行某些操作,而其中某些操作可能无法执行。

这是您必须在设计级别上解决的问题。您可以编写不分配适合“ STL”范例的内存的容器。您可以避免链接列表数据结构,或限制其使用。您可以将结构数组重组为数组或其他结构。它会更改某些内容,但是您可以将其分隔开。

将复杂的RTTI操作更改为常规的虚函数调用?那是一个设计问题。如果您必须进行更改,则需要更改以下内容每个派生类进行。它更改了许多代码与各种类的交互方式。此类更改的范围远远超出了代码的性能关键部分。

那么...您为什么以错误的方式开始写呢?

我不必在不需要它们的地方定义属性或方法,基节点类可以保持精简和有意义。

为了什么目的

您说基类是“瘦弱而卑鄙的”。但是真的...这是不存在的。它实际上什么也没

只要看一下您的示例:node_base。它是什么?这似乎是与其他事物相邻的事物。这是一个Java接口(当时是泛型Java):一个类,仅作为用户可以转换为型的类而存在。也许您添加了一些基本功能,例如邻接(Java添加ToString),仅此而已。

“精益和均值”与“透明”之间有区别。

如Yakk所说,这样的编程风格限制了自己的互操作性,因为如果所有功能都在派生类中,则该系统外部的用户无法访问该派生类,则无法与该系统进行互操作。他们无法覆盖虚拟函数并添加新行为。他们甚至不能调用那些函数。

但是他们也要做的是,即使在系统内部,实际上也很难做新的事情。考虑一下您的poke_adjacent_oranges功能。如果有人想要lime_node可以像orange_nodes 一样戳的类型会发生什么?好吧,我们不能lime_nodeorange_node;这是没有意义的。

相反,我们必须添加一个lime_node从派生的新对象node_base。然后将的名称更改poke_adjacent_orangespoke_adjacent_pokables。然后,尝试投射到orange_nodelime_node; 无论哪种铸件作品都是我们戳的。

但是,lime_node需要它自己 poke_adjacent_pokables。并且此功能需要执行相同的转换检查。

如果添加第三种类型,我们不仅必须添加自己的函数,还必须更改其他两个类中的函数。

显然,现在您可以创建poke_adjacent_pokables一个自由功能,以便它适用于所有功能。但是,如果有人添加第四种类型却忘记将其添加到该函数中,您会怎么办?

您好,无声破损。该程序看起来或多或少可以正常工作,但事实并非如此。如果poke实际的虚函数,则当您未从中重写纯虚函数时,编译器将失败node_base

用您的方式,您没有这样的编译器检查。哦,可以肯定,编译器不会检查非纯虚拟机,但是至少在有可能的保护的情况下(即:没有默认操作),您才能获得保护。

RTTI使用透明基类会导致维护噩梦。实际上,RTTI的大多数使用都会导致维护难题。这并不意味着RTTI没有boost::any例如,对于进行工作至关重要)。但它是一个非常专业的工具可以满足专业的需求。

这样,它与相同,是“有害的” goto。这是一个不应该被淘汰的有用工具。但是在您的代码中很少使用它。


因此,如果您不能使用透明基类和动态转换,如何避免胖接口?如何避免冒泡您可能要调用的每个函数,避免冒泡至基类呢?

答案取决于基类的用途。

像这样的透明基类node_base只是使用了错误的工具来解决问题。链接列表最好由模板处理。节点类型和邻接性将由模板类型提供。如果要在列表中放入多态类型,可以。只需在template参数中使用BaseClass*as即可T。或您首选的智能指针。

但是还有其他情况。一种是可以完成很多事情的类型,但是有一些可选的部分。一个特定的实例可能实现某些功能,而另一个则没有。但是,这种类型的设计通常会提供适当的答案。

“实体”类就是一个很好的例子。此类课程早已困扰游戏开发人员。从概念上讲,它具有巨大的界面,位于几乎十二个完全不同的系统的交叉点上。并且不同的实体具有不同的属性。一些实体没有任何视觉表示,因此其渲染功能不起作用。而这一切都在运行时确定。

对此的现代解决方案是组件样式的系统。Entity仅仅是一组组件的容器,它们之间有一些胶水。一些组件是可选的。没有视觉表示的实体不具有“图形”组件。没有AI的实体没有“控制器”组件。依此类推。

这种系统中的实体只是指向组件的指针,它们的大部分接口都是通过直接访问组件来提供的。

开发这样的组件系统需要在设计阶段就认识到某些功能在概念上被分组在一起,以便实现一个功能的所有类型都可以实现它们。这使您可以从预期的基础类中提取该类,并将其作为单独的组件。

这也有助于遵循“单一责任原则”。这样的组件化类仅负责成为组件的持有者。


从Matthew Walton:

我注意到很多答案并没有注意到您的示例建议node_base是库的一部分,并且用户将创建自己的节点类型。然后他们无法修改node_base以允许其他解决方案,因此RTTI可能会成为他们的最佳选择。

好的,让我们探讨一下。

为了使这一点有意义,您将不得不遇到某些情况,其中某些库L提供了一个容器或其他结构化的数据持有者。用户可以向该容器中添加数据,遍历其内容等。它只是管理它的存在。

但是它甚至不能破坏它的存在。原因是,如果期望您将RTTI用于此类目的,那么您将创建L不了解的类。这意味着您的代码将分配对象并将其交给L进行管理。

现在,在某些情况下,这样的设计是合法的。事件信号/消息传递,线程安全的工作队列等。这里的一般模式是:某人正在执行适用于任何类型的两段代码之间的服务,但是该服务不必知道所涉及的特定类型。

在C中,此模式为拼写void*,并且在使用它时需要格外小心,以免被破坏。在C ++中,此模式是拼写的std::experimental::any(即将被拼写std::any)。

应该起作用的方式是L提供了一个node_base类,该类采用any表示您的实际数据的。当您收到消息,线程队列工作项或您正在执行的任何操作时,然后将any其强制转换为发送者和接收者都知道的适当类型。

因此,而不是获得的orange_node来自node_data,你简单地把一个orange内部node_dataany成员字段。最终用户any_cast将其提取出来并用于将其转换为orange。如果强制转换失败,则不是orange

现在,如果您完全熟悉的实现any,您可能会说:“等等,any 内部使用RTTI进行any_cast工作。” 我对此回答:“ ...是”。

这就是抽象的重点。深入细节中,有人在使用RTTI。但是就您应该操作的水平而言,直接RTTI并不是您应该做的事情。

您应该使用提供所需功能的类型。毕竟,您并不是真的想要RTTI。您需要的是一种数据结构,该结构可以存储给定类型的值,对除所需目标位置以外的所有对象隐藏该值,然后将其转换回该类型,并验证存储的值实际上是该类型。

叫做any。它使用 RTTI,但使用any远远优于直接使用RTTI,因为它可以更正确地符合所需的语义。


10

如果您调用一个函数,那么通常来说,您实际上并不在乎它将采取什么精确步骤,而只是在一定的约束范围内实现某些更高级别的目标(而函数如何实现这一点实际上是自己的问题)。

当您使用RTTI预先选择可以完成某项工作的特殊对象时,而同一组中的其他对象则无法做某事,那么您将打破这种舒适的世界观。突然之间,呼叫者应该知道谁可以做,而不是简单地告诉他的仆从继续进行。有些人对此感到不安,我怀疑这是RTTI被认为有点脏的很大一部分原因。

有性能问题吗?也许吧,但我从未经历过,这可能是20年前的智慧,或者是那些诚实地认为使用三个汇编指令而不是两个汇编指令的人的wisdom肿。

因此,如何处理...根据您的情况,将特定于节点的属性捆绑到单独的对象中是有意义的(即,整个“橙色” API可以是单独的对象)。然后,根对象可以具有虚拟函数以返回“橙色” API,默认情况下,对于非橙色对象,返回nullptr。

尽管这可能会因您的情况而过大,但它允许您在根级别上查询特定节点是否支持特定API,如果支持,则执行特定于该API的功能。


6
回复:性能成本-我在3 GHz处理器上的应用程序中测得dynamic_cast <>的成本约为2µs,这比检查枚举要慢1000倍。(我们的应用程序的主循环截止日期为11.1ms,因此我们非常在意微秒。)
Crashworks

6
不同实现之间的性能差异很大。GCC使用typeinfo指针比较是快速的。MSVC使用不快速的字符串比较。但是,MSVC的方法将与链接到不同版本的库(静态或DLL)的代码一起使用,其中GCC的指针方法认为静态库中的类不同于共享库中的类。
Zan Lynx

1
@Crashworks在这里只是要有完整的记录:那是哪个编译器(和哪个版本)?
H. Guijt

@Crashworks要求提供有关哪个编译器产生了您所观察到的结果的信息的请求;谢谢。
underscore_d

@underscore_d:MSVC。
Crashworks

9

C ++建立在静态类型检查的思想之上。

[1] RTTI(即dynamic_casttype_id)是动态类型检查。

因此,本质上您是在问为什么静态类型检查优于动态类型检查。简单的答案是,静态类型检查是否优于动态类型检查取决于。在很多。但是C ++是围绕静态类型检查的思想设计的编程语言之一。这就意味着,例如,开发过程(尤其是测试过程)通常适合于静态类型检查,然后才最适合。


回覆

我不知道使用模板或其他方法进行此操作的干净方法

您可以使用静态类型检查来执行此过程异构图节点,而不会通过访问者模式进行任何类型的转换,例如:

#include <iostream>
#include <set>
#include <initializer_list>

namespace graph {
    using std::set;

    class Red_thing;
    class Yellow_thing;
    class Orange_thing;

    struct Callback
    {
        virtual void handle( Red_thing& ) {}
        virtual void handle( Yellow_thing& ) {}
        virtual void handle( Orange_thing& ) {}
    };

    class Node
    {
    private:
        set<Node*> connected_;

    public:
        virtual void call( Callback& cb ) = 0;

        void connect_to( Node* p_other )
        {
            connected_.insert( p_other );
        }

        void call_on_connected( Callback& cb )
        {
            for( auto const p : connected_ ) { p->call( cb ); }
        }

        virtual ~Node(){}
    };

    class Red_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        auto redness() -> int { return 255; }
    };

    class Yellow_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }
    };

    class Orange_thing
        : public Red_thing
        , public Yellow_thing
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        void poke() { std::cout << "Poked!\n"; }

        void poke_connected_orange_things()
        {
            struct Poker: Callback
            {
                void handle( Orange_thing& obj ) override
                {
                    obj.poke();
                }
            } poker;

            call_on_connected( poker );
        }
    };
}  // namespace graph

auto main() -> int
{
    using namespace graph;

    Red_thing   r;
    Yellow_thing    y1, y2;
    Orange_thing    o1, o2, o3;

    for( Node* p : std::initializer_list<Node*>{ &y1, &y2, &r, &o2, &o3 } )
    {
        o1.connect_to( p );
    }
    o1.poke_connected_orange_things();
}

这假定节点类型集是已知的。

如果不是这样,则可以使用一些集中式的类型转换,或仅一个单一的形式来表示访问者模式(它有很多变体)。


有关基于模板的方法,请参见Boost Graph库。可悲的是我不熟悉它,我没有用它。因此,我不确定它的确切功能,工作方式以及使用静态类型检查而不是RTTI的程度,但是由于Boost通常是基于模板的静态类型检查为中心思想,因此我认为您会发现它的Graph子库也基于静态类型检查。


[1] 运行时类型信息


1
要注意的一个“有趣的事情”是,可以减少访问者模式所需的代码量(添加类型时的更改),方法是使用RTTI“爬升”层次结构。我知道这是“非循环访客模式”。
Daniel Jour

3

当然,在某些情况下,多态性无济于事:名称。typeid尽管该名称的编码方式是实现定义的,但您仍可以访问类型的名称。但这通常不是问题,因为您可以比较两个typeid-s:

if ( typeid(5) == "int" )
    // may be false

if ( typeid(5) == typeid(int) )
   // always true

哈希也是如此。

[...] RTTI被认为“有害”

有害肯定是夸大其词:RTTI有一些缺点,但它有优点。

您不必真正使用RTTI。RTTI是解决OOP问题的工具:如果您使用其他范例,这些范例可能会消失。C没有RTTI,但仍然有效。C ++而不是完全支持OOP并为您提供多种工具,以克服一些问题,可能需要运行时信息:其中一个确实RTTI,这虽然是有代价的。如果您负担不起,最好只经过安全的性能分析后再说,仍然有些老套void*:它是免费的。无成本的。但是您没有类型安全性。因此,一切都与交易有关。


  • 一些编译器不使用/ RTTI并不总是启用的,
    我真的不买这个参数。这就像说我不应该使用C ++ 14功能,因为那里有不支持它的编译器。但是,没有人会阻止我使用C ++ 14功能。

如果编写(可能严格地)符合C ++的代码,则无论实现如何,都可以期待相同的行为。符合标准的实现应支持标准C ++功能。

但是请务必考虑在某些C ++定义的环境(“独立”环境)中,无需提供RTTI,也不需要提供异常,virtual等等。RTTI需要一个底层来正常工作,该底层处理诸如ABI和实际类型信息之类的底层细节。


在这种情况下,我同意Yakk的RTTI。是的,可以使用;但这在逻辑上是正确的吗?语言允许您绕过该检查的事实并不意味着应该进行此检查。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.