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

mbr*_*0wn 104 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的解决方案,这通常意味着我将不得不在我的类中添加一些字段来存储这些信息,因此内存参数有点无效(我将进一步说明这一点).

事实上,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();
};
Run Code Online (Sandbox Code Playgroud)

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

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);
};
Run Code Online (Sandbox Code Playgroud)

见鬼,为什么不是其中之一:

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

    void poke();
    void poke_adjacent_oranges();
};
Run Code Online (Sandbox Code Playgroud)

最后一个功能很有趣.这是一种写它的方法:

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这一切看起来都很清晰.我没有必要定义不需要它们的属性或方法,基类节点可以保持精简和平均.没有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;
};
Run Code Online (Sandbox Code Playgroud)

std :: string对于一个类型是个好主意吗?也许不是,但我还能用什么呢?弥补一个数字,并希望没有其他人使用它?另外,在我的orange_node的情况下,如果我想使用red_node和yellow_node中的方法怎么办?我是否必须为每个节点存储多个类型?这似乎很复杂.

结论

这个例子看起来并不复杂或不寻常(我在日常工作中处理类似的事情,其中​​节点代表通过软件控制的实际硬件,并且根据它们的不同,它们会做出非常不同的事情).然而,我不知道用模板或其他方法做到这一点的干净方法.请注意,我正在尝试理解这个问题,而不是为我的例子辩护.我阅读上面链接的SO回答以及Wikibooks上的这个页面似乎表明我在滥用RTTI,但我想了解原因.

那么,回到我原来的问题:为什么'纯多态'比使用RTTI更可取?

Yak*_*ont 68

界面描述了在代码中给定情况下进行交互时需要知道的内容.一旦您使用"整个类型层次结构"扩展界面,您的界面"表面区域"就会变得庞大,这使得对它的推理更加困难.

举个例子,你的"邻近橙子戳"意味着我作为第三方,不能模仿橙色!您私下声明了橙色类型,然后使用RTTI使您的代码在与该类型交互时表现得特别.如果我想"变成橙色",我必须在你的私人花园里.

现在,每个与"橘子"结合的人都会与你的整个橙色类型结合,而且与你的整个私人花园一起,而不是与一个明确的界面.

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

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

class node_base {
  public:
    bool has_tag(tag_name);
Run Code Online (Sandbox Code Playgroud)

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

如果需要,甚至可以将其扩展到动态方法."你是否支持与Baz,Tom和Alice争吵?好吧,你好." 从很大程度上讲,这比动态演员更少侵入,因为其他对象是你所知道的类型.

现在,橘子对象可以具有橙色标签并且可以一起播放,同时实现 - 解耦.

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

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

  • 最后一段+1; 不仅因为我同意你的意见,而且因为这是在这里的锤子. (14认同)
  • 一旦知道对象被标记为支持该功能,如何获得特定功能?无论是涉及施法,还是上帝班都有各种可能的成员功能.第一种可能性是未经检查的转换,在这种情况下,标记只是一个自己的非常易错的动态类型检查方案,或者它被检查`dynamic_cast`(RTTI),在这种情况下标记是多余的.第二种可能性,即上帝阶级,是令人憎恶的.总而言之,这个答案有很多我认为对Java程序员来说听起来不错的词,但实际内容毫无意义. (7认同)
  • @Falco:这是我提到的第一种可能性(一种变体),基于标签取消选中.这里标记是一个非常脆弱且非常易错的动态类型检查方案.任何小客户端代码的不当行为,而在C++中,其中一个是UB-land.您不会像Java中那样获得异常,但会获得未定义的行为,例如崩溃和/或不正确的结果.除了非常不可靠和危险之外,与更加理智的C++代码相比,它的效率也非常低.IOW.,它非常非常好; 非常如此. (2认同)
  • @JojOatXGME:因为"多态"意味着能够使用各种类型.如果你必须检查它是否是*特定的*类型,除了用于获取指针/引用的现有类型检查之外,那么你正在寻找多态性.你没有使用各种类型的工作; 你正在使用*特定*类型.是的,有"Java中的(大)项目"可以做到这一点.但那是*Java*; 该语言只允许动态多态.C++也有静态多态性.而且,仅仅因为有人"大"并不是一个好主意. (2认同)

Emi*_*lia 31

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

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

他们拥有我曾经称之为"管道工综合体"的东西:他们认为所有的水龙头都出现故障,因为它们被称为修理的所有水龙头都是.现实情况是,大多数水龙头工作得很好:你根本就不会为他们打电话给水管工!

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

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

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

这个想法是:

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

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

如果有许多情况在编译时都已知,但您不知道它们必须采取的实际数据,则编译时多态(主要是CRTP或类似的)可以是一种解决方案.

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

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

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

如果不仅仅是切换,但即使动作不是已知的编译时间,也需要脚本和解析:数据本身必须描述要对它们采取的动作.

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

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

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

  • “可能发生的一件疯狂的事情是,为了避免使用某个给定的功能,程序员编写了大量样板代码,实际上私下重新实现了该功能。”[mashes upvote 按钮]“为了抨击 RTTI 而只是为了抨击它,就像抨击它一样`goto` 只是为了抨击它。这是鹦鹉学舌的东西,而不是程序员的东西。” [继续捣碎,徒劳地]除此之外,这个模式的_精彩_总结、它对 RTTI 的适用性以及替代方案。对于那些被教导讨厌锤子的人来说,每个钉子都是……嗯,锤子……哦,你知道我的意思。 (2认同)

Bo *_*son 23

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

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

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


一如既往,很难创建通用示例,我会给你.

但是如果我要做这个具体的例子,我会poke()在所有类中添加一个成员,void poke() {}如果他们不感兴趣,就让其中一些人忽略call().

当然,这比比较typeids 更便宜.

  • 你说"肯定",但是什么让你这么肯定?这就是我想要弄清楚的.假设我将orange_node重命名为pokable_node,它们是我可以调用poke()的唯一命令.这意味着我的接口需要实现一个poke()方法,比如抛出一个异常("这个节点不可以").这似乎*更贵*. (3认同)
  • 为什么他需要抛出异常?如果您关心接口是否"可以",只需添加一个函数"isPokeable"并在调用poke函数之前先调用它.或者只是按照他所说的去做,"在非可扩展的课堂上什么都不做". (2认同)
  • @NicolBolas 为什么您希望友好和敌对的怪物共享相同的基类,或者可聚焦和不可聚焦的 UI 元素,或者带小键盘的键盘和不带小键盘的键盘? (2认同)

Nic*_*las 18

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

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

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

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

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

在热循环中你有很多事情没做.你没有分配内存.你不去迭代链表.等等.RTTI当然可以是另外一个"不要在这里做这件事"的事情.

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

这是你必须在设计层面解决的问题.您可以编写不分配符合"STL"范例的内存的容器.您可以避免链表数据结构,或限制其使用.您可以将结构数组重组为数组或其他结构.它改变了一些东西,但你可以把它分开.

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

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

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

到底是什么?

你说基类是"精益和平均".但实际上......它是不存在的.它实际上没有做任何事情.

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

"精益与平均"和"透明"之间存在差异.

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

但他们也做的是让实际做新事物的主要痛苦,即使在系统内也是如此.考虑你的poke_adjacent_oranges功能.如果有人想要一种lime_nodeorange_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只是使用错误的工具来解决问题.链接列表最好由模板处理.节点类型和邻接将由模板类型提供.如果要在列表中放置多态类型,则可以.只需使用BaseClass*作为T模板参数.或者您首选的智能指针.

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

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

现代解决方案是组件式系统.Entity它只是一组组件的容器,它们之间有一些粘合剂.一些组件是可选的; 没有可视化表示的实体没有"图形"组件.没有AI的实体没有"控制器"组件.等等.

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

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

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


来自马修沃尔顿:

我注意到很多答案都没有注意到你的例子表明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_dataorangenode_dataanyany_castorangeorange

现在,如果你完全熟悉它的实现any,你可能会说,"嘿等一下:any 内部使用RTTI来any_cast开展工作." 我回答说,"......是的".

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

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

这就是所谓的any.它使用 RTTI,但使用any远远优于直接使用RTTI,因为它更准确地符合所需的语义.


H. *_*ijt 10

如果你调用一个函数,作为一项规则,你真的不关心它会采取什么具体步骤,只有一些更高层次的目标将一定的约束内实现(和功能如何让这种情况发生确实是它自己的问题).

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

是否存在性能问题?也许吧,但我从来没有经历过它,它可能是从智慧二十年前,还是从人谁真的相信使用,而不是二三汇编指令是不可接受的膨胀.

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

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

  • Re:性能成本 - 我测量的dynamic_cast <>在我们的应用程序中在3GHz处理器上耗费约2μs,比检查枚举慢约1000倍.(我们的应用程序有一个11.1ms的主循环截止时间,因此我们非常关心微秒.) (6认同)
  • 实现之间的性能差异很大.GCC使用快速的typeinfo指针比较.MSVC使用不快的字符串比较.但是,MSVC的方法将使用链接到不同版本的库(静态或DLL)的代码,其中GCC的指针方法认为静态库中的类与共享库中的类不同. (6认同)

Che*_*Alf 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();
}
Run Code Online (Sandbox Code Playgroud)

这假设已知节点类型集.

如果不是,访问者模式(有很多变体)可以用一些集中的演员表达,或者只用一个表达.


对于基于模板的方法,请参阅Boost Graph库.不幸的是,我不熟悉它,我没有用它.所以我不确定它究竟是做什么以及如何做,以及它在多大程度上使用静态类型检查而不是RTTI,但由于Boost通常是基于模板的,以静态类型检查为中心思想,我想你会发现它的Graph子库也基于静态类型检查.


[1] 运行时类型信息.