为什么'pure polymorphism'比使用 RTTI 更可取?

Why is 'pure polymorphism' preferable over using RTTI?

本文关键字:RTTI pure polymorphism 为什么      更新时间:2023-10-16

我看到的几乎每一个讨论这类事情的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();
};

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

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答案和维基解密上的这个页面,似乎表明我滥用了RTTI,但我想知道为什么。

那么,回到我最初的问题:为什么"纯多态性"比使用RTTI更可取

接口描述了在给定的代码情况下进行交互所需了解的内容。一旦你用"你的整个类型层次结构"扩展了接口,你的接口"表面积"就会变得巨大,这使得对它的推理变得更加困难。

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

现在,每个与"橙色"配对的人都与你的整个橙色类型配对,并隐含地与你的全部私人花园配对,而不是与一个定义的界面配对。

乍一看,这似乎是一种扩展有限接口而不必更改所有客户端(添加am_I_orange)的好方法,但实际情况往往是它使代码库僵化,并且阻止进一步扩展。特殊的橙色成为系统功能所固有的,并阻止你为橙色创建一个"橙色"替代品,该替代品以不同的方式实现,可能会消除依赖性或优雅地解决其他问题。

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

class node_base {
public:
bool has_tag(tag_name);

这为您的界面提供了类似的大规模扩展,从狭义指定扩展到基于广泛标记。除了没有通过RTTI和实现细节(也就是说,"你是如何实现的?用橙色类型?好吧,你通过了。"。

这甚至可以扩展到动态方法,如果需要的话。"你支持用Baz、Tom和Alice的论点来愚弄你吗?好吧,愚弄你。"从很大的意义上说,这比动态转换来了解另一个对象是你知道的类型要少的侵入性。

现在,tangerine对象可以具有橙色标记并进行播放,同时实现解耦。

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

抽象是一种脱钩和隐藏无关事物的游戏。它使代码更容易在本地推理。RTTI在实现细节的抽象中钻了一个洞。这可以使解决问题变得更容易,但它的代价是非常容易地将您锁定到一个特定的实现中。

大多数反对这一或那个特征的道德劝说都是典型性的,源于对该特征的一些误解。

道德家的失败之处在于,他们认为所有的用法都是错误的,而事实上特征的存在是有原因的。

他们有我过去称之为"水管工复合体"的东西:他们认为所有水龙头都有故障,因为他们被要求修理的所有水龙头都有故障。事实上,大多数水龙头都能正常工作:你根本不需要给水管工打电话!

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

关于多态性,有一种通用的方法:IF(selection) CALL(something) WITH(parameters)。(对不起,编程,当忽略抽象时,就是这样)

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

想法是:

您可以预测的越多,就越有可能发现错误并避免影响最终用户的错误

如果一切都是恒定的(包括数据),那么您可以使用模板元编程来完成所有操作。在编译实现的常量之后,整个程序可以归结为一个返回语句,该语句吐出结果

如果有许多情况在编译时都是已知的,但你不知道它们必须处理的实际数据,那么编译时多态性(主要是CRTP或类似的)可能是一个解决方案。

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

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

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

如果不仅切换,甚至操作在编译时都不已知,那么脚本&解析是必需的:数据本身必须描述要对其执行的操作。

现在,由于我列举的每一个案例都可以被视为后面的一个特定案例,所以你可以通过滥用最底层的解决方案来解决每一个问题,也可以针对最顶层负担得起的问题。

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

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

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

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

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


和往常一样,很难创建通用示例,我会告诉你。

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

毫无疑问,这将比比较typeid的更便宜

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

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

有许多C++编码位置不使用RTTI。其中编译器开关用于强制禁用RTTI。如果你在这样一个范式中编码。。。那么你几乎肯定已经被告知了这个限制。

因此,问题出在上。也就是说,如果您正在编写一个依赖RTTI的库,那么关闭RTTI的用户就不能使用您的库。如果你想让这些人使用你的库,那么它就不能使用RTTI,即使你的库也被可以使用RTTI的人使用。同样重要的是,如果你不能使用RTTI,你就必须更加努力地货比三家来购买库,因为RTTI的使用会破坏你的交易。

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

在热循环中有很多事情是不能做的。您不分配内存。您不会遍历链接列表。等等RTTI当然可以是另一种"不要在这里做这件事"。

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

这是您必须在设计级别上解决的问题。您可以编写不分配符合"STL"范式的内存的容器。可以避免使用链表数据结构,或限制其使用。您可以将结构数组重新组织为数组结构或其他任何结构。它改变了一些事情,但你可以把它划分开来。

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

所以。。。你为什么一开始就写错了?

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

目的是什么?

你说基础阶级是"瘦而吝啬"的。但实际上。。。它不存在。它实际上并没有做任何事情

看看你的例子:node_base。它是什么?它似乎是一个与其他事物相邻的事物。这是一个Java接口(在这里是Java的前泛型):一个类的存在只是为了让用户可以转换为real类型。也许你添加了一些基本功能,比如邻接(Java添加了ToString),但仅此而已

"吝啬"answers"透明"是有区别的。

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

但他们所做的也是让真正做新事情成为一种巨大的痛苦,即使是在系统内。考虑您的poke_adjacent_oranges函数。如果有人想要一个可以像orange_node一样戳的lime_node类型,会发生什么?好吧,我们不能从orange_node推导出lime_node;这毫无意义。

相反,我们必须添加一个源自node_base的新lime_node。然后将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的工作至关重要)。但它是一个非常专业的工具,满足非常的专业需求。

这样,它就和goto一样"有害"。这是一个有用的工具,不应该被废除。但在您的代码中,它的使用应该是罕见的。


那么,如果不能使用透明基类和动态铸造,如何避免胖接口?如何避免对类型调用的每个函数都冒泡到基类?

答案取决于基类的用途。

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

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

"entity"类就是一个很好的例子。这个类长期以来一直困扰着游戏开发者。从概念上讲,它有一个巨大的界面,生活在近十几个完全不同的系统的交叉点上。不同的实体具有不同的属性。有些实体没有任何视觉表示,因此它们的渲染功能不起任何作用。这一切都是在运行时决定的。

这方面的现代解决方案是组件式系统。Entity只是一组组件的容器,它们之间有一些胶水。某些组件是可选的;没有视觉表示的实体没有"图形"组件。没有人工智能的实体没有"控制器"组件。等等

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

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

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


来自Matthew Walton:

我注意到很多答案没有注意到你的例子表明node_base是库的一部分,用户将创建自己的节点类型。然后他们不能修改node_base以允许另一个解决方案,所以RTTI可能会成为他们的最佳选择。

好的,让我们来探讨一下。

为了使这一点有意义,您必须有这样一种情况:某个库L提供了一个容器或其他结构化的数据持有者。用户可以将数据添加到这个容器中,对其内容进行迭代等。然而,库并没有真正处理这些数据;它只是管理自己的存在。

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

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

在C中,这个模式拼写为void*,使用它需要非常小心,以免被破坏。在C++中,这个模式的拼写是std::experimental::any(不久将拼写为std::any)。

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

因此,不需要从node_data派生orange_node,只需在node_dataany成员字段中插入一个orange即可。最终用户提取它并使用any_cast将它转换为orange。如果强制转换失败,则不是orange

现在,如果您完全熟悉any的实现,您可能会说:"嘿,等一下:any内部使用RTTI使any_cast工作。"对此,我回答:"…是的"。

这就是抽象的要点。在细节的深处,有人在使用RTTI。但在你应该操作的水平上,直接RTTI不是你应该做的事情。

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

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

如果你调用一个函数,通常你并不关心它会采取什么样的精确步骤,只关心在某些约束条件下会实现一些更高级别的目标(函数如何实现这一点实际上是它自己的问题)。

当你使用RTTI来预选可以完成某项工作的特殊对象,而同一集中的其他对象则不能,你就打破了对世界的舒适看法。突然之间,打电话的人应该知道谁能做什么,而不是简单地告诉他的小黄人继续做下去。有些人对此感到困扰,我怀疑这是RTTI被认为有点脏的很大一部分原因。

是否存在性能问题?也许吧,但我从来没有经历过,这可能是20年前的智慧,也可能是那些诚实地认为使用三个组装说明而不是两个组装说明是不可接受的膨胀的人的智慧。

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

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

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

当然,在一种情况下多态性是没有帮助的:名称。typeid允许您访问类型的名称,尽管该名称的编码方式是由实现定义的。但通常这不是问题,因为你可以比较两个typeid

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。是的,可以使用;但它在逻辑上正确吗?语言允许您绕过此检查并不意味着应该这样做。

如果您可以在编译时枚举参与图的类型集,则可以将dynamic_cast()的使用替换为std::variant。这应该更有效率。它还创建了将未处理的节点类型变成编译时错误的可能性。当然,节点类型的集合可能只有在运行时才知道,然后您几乎肯定需要某种RTTI。https://godbolt.org/z/TPjxa1G6M

#include <unordered_map>
#include <variant>
#include <type_traits>
#include <cstdio>
template <class... Ts>
struct graph {
using key = std::variant<Ts const*...>;
using value = std::variant<Ts*...>;
std::unordered_multimap<key, value> edge;
template <class callable>
void visit_adjacent(key n, callable f) const {
auto [i, e] = edge.equal_range(n);
for (; i != e; ++i) {
std::visit(f, i->second);
}
}
};

struct red {};
struct yellow {};
struct orange : red, yellow {
void poke() { std::printf("poke %pn", (void*)this); }
template <class graph>
void poke_adjacent_oranges(graph const& g) const {
g.visit_adjacent(this, []<class T>(T* other) {
if constexpr (std::is_base_of_v<orange, T>) {
other->poke();
}
});
}
};
// ....
struct blue {};
int
main(){
graph<red, yellow, orange, blue> g;
orange o1;
orange o2;
red r;
blue b;
g.edge.emplace(&o1, &o2);
g.edge.emplace(&o1, &r);
g.edge.emplace(&o1, &b);
std::printf("o2: %pn", (void*)&o2);
o1.poke_adjacent_oranges(g);

return 0;
}