为什么'纯多态性'优于使用RTTI?

时间:2016-03-03 06:40:19

标签: c++ polymorphism rtti

我见过的几乎所有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的解决方案,这通常意味着我必须在我的类中添加一些字段来存储这些信息,所以内存参数有点无效(我将给出一个这样的例子) )。

动态广播确实很慢。但是,通常有办法避免使用速度危急情况。而且我也没有完全看到替代方案。 This SO answer建议使用在基类中定义的枚举来存储类型。这只有在您了解所有派生类时才有效。这是一个非常大的问题,如果"!

从这个答案来看,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回答和this page on Wikibooks之类的页面似乎表明我误用了RTTI,但我想了解原因。

所以,回到我原来的问题:为什么&#39;纯多态性&#39;优于使用RTTI?

7 个答案:

答案 0 :(得分:68)

接口描述了在代码中的给定情况下进行交互时需要知道的内容。一旦您使用&#34;整个类型层次&#34;扩展界面,您的界面&#34;表面区域&#34;变得巨大,这使得它的推理更难

作为一个例子,你的&#34;戳相邻的橘子&#34;意味着我,作为第三方,不能效仿橙色!您私下声明了橙色类型,然后使用RTTI使您的代码在与该类型交互时表现得特别。如果我想&#34;是橙色&#34;,我必须在您的私人花园内。

现在每个与橘子情侣结合的人#34;与您的整个橙色类型结合,并与您的整个私人花园隐含,而不是使用已定义的界面。

乍一看,这看起来像是一种扩展有限界面而不必更改所有客户端的好方法(添加am_I_orange),而往往发生的事情是它会使代码库僵化,并且可以防止进一步扩展。特殊的橙色成为系统功能所固有的,并且可以防止你创造一个橘子&#34;替换为不同实现的橙色,可能会删除依赖关系或优雅地解决其他问题。

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

class node_base {
  public:
    bool has_tag(tag_name);

这提供了类似的大规模扩展您的界面从狭窄指定到基于广泛标记。除了通过RTTI和实现细节而不是这样做(又名,&#34;你是如何实现的?使用橙色类型?好的,你通过了。&#34;),它通过一个完全不同的实现轻松模拟的东西。

这甚至可以扩展到动态方法,如果你需要它。 &#34;你支持Foo与Baz,Tom和Alice争吵吗?好的,求你了。&#34;从很大程度上讲,这是 less 侵入性而不是动态强制转换,以获得另一个对象是您所知道的类型。

现在,橘子对象可以使用橙色标记并一起播放,同时实现解耦。

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

抽象是一种脱钩和隐藏无关的游戏。它使代码更容易在本地推理。 RTTI直接通过抽象实现细节。这可以使问题更容易解决,但是它很容易将您锁定到一个特定的实现中。

答案 1 :(得分:31)

针对这个或那个特征的道德劝说的大多数是典型性源于观察到该特征存在一些错误的使用。

道德主义者失败的地方在于他们认为所有用法都是错误的,而实际上特征存在是有原因的。

他们拥有我曾经称之为“管道工综合体”的东西:他们认为所有 taps 发生故障因为他们被调用的所有水龙头都是。现实情况是,大多数水龙头工作得很好:你根本就不会为他们打电话给管道工!

一个可能发生的疯狂事情是,为了避免使用给定的功能,程序员编写了大量的样板代码,实际上私下重新实现了该功能。 (您是否遇到过不使用RTTI和虚拟调用的类,但是有值来跟踪它们的实际派生类型?这不过是伪装的 RTTI 重新创建。)

有一种考虑多态性的一般方法:IF(selection) CALL(something) WITH(parameters)。 (抱歉,编程时,无视抽象,就是这样)

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

这个想法是:

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

如果一切都是不变的(包括数据),您可以使用模板元编程完成所有工作。在实现的常量上进行编译之后,整个程序归结为只返回结果的返回语句。

如果有许多案例在编译时都已知 ,但你不知道他们必须采取行动的实际数据,那么编译时多态(主要是CRTP或类似的)可以是一个解决方案。

如果案例的选择取决于数据(不是编译时已知值)并且切换是单维的(可以做什么只能减少到一个值)那么基于虚拟函数的调度(或通常“需要函数指针表“。”

如果切换是多维的,因为C ++中不存在本机多个运行时调度,那么您必须:

  • 通过 Goedelization 缩小到一个维度:这是虚拟基础和多重继承的地方,钻石堆叠的平行四边形,但这需要可知组合的数量,并且相对较小。
  • 将尺寸链接到另一个(就像在复合访问者模式中一样,但这需要所有类都知道他们的其他兄弟姐妹,因此它不能从它所构思的地方“缩小”)
  • 根据多个值调度调用。 这正是RTTI的用途。

如果不仅仅是切换,但即使动作不是已知的编译时间,那么脚本&amp;解析是必需的:数据本身必须描述要对它们采取的行动。

现在,由于我列举的每个案例都可以看作是其后的特例,您可以通过滥用最底层的解决方案来解决所有问题,也可以解决最顶层的问题。

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

抨击RTTI只是为了抨击它,就像抨击goto只是为了抨击它。鹦鹉的事情,而不是程序员。

答案 2 :(得分:23)

在一个小例子中看起来很整洁,但在现实生活中,你很快就会得到一长串可以互相戳的类型,其中一些可能只在一个方向上。

dark_orange_nodeblack_and_orange_striped_nodedotted_node呢?可以有不同颜色的点吗?如果大多数点都是橙色,那么它可以被戳吗?

每次必须添加新规则时,您必须重新访问所有poke_adjacent函数并添加更多if语句。

一如既往,很难创建通用示例,我会告诉你。

但是,如果我要执行此特定的示例,我会向所有类添加poke()成员,并让其中一些成员忽略该调用(void poke() {})他们不感兴趣。

肯定比比较typeid更便宜。

答案 3 :(得分:18)

  

有些编译器没有使用它/ RTTI并不总是启用

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

有许多C ++编码场所不使用RTTI。编译器开关用于强制禁用RTTI的位置。如果你在这样的范例内编码......那么你几乎肯定已经知道了这种限制。

因此问题在于。也就是说,如果您正在编写依赖于RTTI的库,那么关闭RTTI的用户就无法使用您的库 。如果您希望这些人使用您的库,那么即使您的库也被可以使用RTTI的人使用,它也无法使用RTTI。同样重要的是,如果你不能使用RTTI,你必须更加努力地购买图书馆,因为RTTI的使用对你来说是一个交易破坏者。

  

需要额外的内存/可能很慢

在热循环中你有很多事情要做。你没有分配内存。你不会遍历链表。等等。 RTTI当然可以是另外一个&#34;不要在这里做到这一点&#34;的东西。

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

您需要在设计级别上解决这个问题。你可以编写不会分配内存的容器,这些容器适合于&#34; STL&#34;范例。您可以避免链表数据结构,或限制其使用。您可以将结构数组重组为数组或其他结构。它会改变一些事情,但你可以把它分开。

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

那么......为什么你开始用错误的方式写它?

  

我不必定义我不需要它们的属性或方法,基类节点可以保持精益和平均值。

到底是什么?

你说基类是&#34;精益和意味着&#34;。但实际上......它不存在。它实际上并没有做任何事情

看看你的例子:node_base。它是什么?这似乎是与其他东西相邻的东西。这是一个Java接口(pre-generics Java):一个只存在于用户可以强制转换为真实类型的类。也许你添加一些基本的功能,如邻接(Java添加ToString),但那就是它。

&#34;精益和平均&#34;之间存在差异。和&#34;透明&#34;。

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

但他们也做的是让实际做新事物的主要痛苦,即使在系统内也是如此。考虑一下您的poke_adjacent_oranges功能。如果有人想要lime_node类型,可以像orange_node那样进行戳,会发生什么?好吧,我们无法从lime_node派生orange_node;这毫无意义。

相反,我们必须添加从lime_node派生的新node_base。然后将poke_adjacent_oranges的名称更改为poke_adjacent_pokables。然后,尝试转换为orange_nodelime_node;无论哪个演员都是我们戳的人。

但是,lime_node需要拥有 poke_adjacent_pokables。此功能需要进行相同的铸造检查。

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

显然,现在你使poke_adjacent_pokables成为一个自由函数,以便它适用于所有这些函数。但是,如果有人添加第四种类型并忘记将其添加到该功能,您会发生什么?

您好,无声破损。该程序似乎或多或少都可以正常工作,但它不是。如果poke实际的虚函数,那么当您没有从node_base覆盖纯虚函数时,编译器就会失败。

按照自己的方式,您没有这样的编译器检查。哦,当然,编译器不会检查非纯虚拟,但至少在有可能保护的情况下你有保护(即:没有默认操作)。

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

通过这种方式,它是有害的&#34;与goto相同。它是一个不应该被废除的有用工具。但是在您的代码中,它的使用应该是罕见的

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

答案取决于基类的用途。

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

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

&#34;实体&#34; class就是一个很好的例子。这个课程早已困扰着游戏开发者。从概念上讲,它有一个巨大的界面,生活在十几个完全不同的系统的交叉点。不同的实体具有不同的属性。有些实体没有任何可视化表示,因此它们的渲染功能不起作用。这一切都是在运行时确定的。

现代解决方案是组件式系统。 Entity仅仅是一组组件的容器,它们之间有一些粘合剂。一些组件是可选的;没有视觉表示的实体没有&#34;图形&#34;零件。没有AI的实体没有&#34;控制器&#34;零件。等等。

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

开发这样的组件系统需要在设计阶段识别某些功能在概念上组合在一起,这样所有实现一个功能的类型都将实现它们。这允许您从预期的基类中提取类,并使其成为一个单独的组件。

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

来自马修沃尔顿:

  

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

好的,让我们探讨一下。

为了理所当然,你必须拥有的是某些库L提供容器或其他结构化数据持有者的情况。用户可以将数据添加到此容器,迭代其内容等。但是,库对此数据没有任何作用;它只是管理它的存在。

但它甚至没有像 destroy 那样管理它的存在。原因在于,如果您希望将RTTI用于此类目的,那么您将创建L不知道的类。这意味着您的代码分配对象并将其交给L进行管理。

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

在C中,此模式拼写为void*,其使用需要非常小心以避免被破坏。在C ++中,此模式拼写为std::experimental::any(很快拼写为std::any)。

应该工作的方式是L提供node_base类,其中any代表您的实际数据。当您收到消息,线程队列工作项或您正在执行的任何操作时,您将any转换为适当的类型,发件人和接收者都知道该类型。

因此,您只需在orange_node node_data成员字段中添加orange,而不是从node_data派生any。最终用户将其抽取并使用any_cast将其转换为orange。如果演员表失败,那么它不是orange

现在,如果您完全熟悉any的实施,您可能会说,&#34;嘿等一下:any 内部使用RTTI使any_cast工作。&#34;我回答,&#34; ......是&#34;。

这是抽象的重点。在细节的深处,有人正在使用RTTI。但是在你应该操作的级别上,直接RTTI不是你应该做的事情。

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

这称为any使用 RTTI,但使用any远远优于直接使用RTTI,因为它更符合所需的语义。

答案 4 :(得分:10)

如果你调用一个函数,作为一项规则,你并不关心它将采取什么样的精确步骤,只有在某些约束条件下才会实现某些更高级别的目标(以及函数如何实现这一目标的确如此)它自己的问题)。

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

是否存在性能问题?也许吧,但我从来没有经历过它,它可能是二十年前的智慧,或者是那些诚实地相信使用三个装配指令而不是两个装配指令的人是不可接受的臃肿。

那么如何处理它...根据您的情况,将任何特定于节点的属性捆绑到单独的对象中可能是有意义的(即整个&#39;橙色&#39; API可能是一个单独的对象) 。然后,根对象可以具有虚函数以返回&#39; orange&#39; API,默认情况下为非橙色对象返回nullptr。

虽然根据您的具体情况可能会有些过分,但它允许您在根级别查询特定节点是否支持特定API,如果是,则执行特定于该API的功能。

答案 5 :(得分:9)

C ++建立在静态类型检查的基础上。

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

所以,基本上你会问为什么静态类型检查比动态类型检查更可取。简单的答案是,静态类型检查是否优于动态类型检查,取决于。很多。但是C ++是围绕静态类型检查思想设计的编程语言之一。这意味着,例如开发过程,特别是测试,通常适用于静态类型检查,然后最适合。

RE

  

我不知道用模板或其他方法做到这一点的干净方法

您可以使用静态类型检查执行此过程 - 异构 - 图形节点,并且无需通过访问者模式进行任何转换,例如:像这样:

#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] 运行时类型信息。 功能

答案 6 :(得分: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的观点。是的,它可以使用;但它在逻辑上是否正确?语言允许您绕过此检查这一事实并不意味着应该这样做。