【问题标题】:C++: doubts about visitor patternC++:对访问者模式的质疑
【发布时间】:2010-11-14 12:20:32
【问题描述】:

我知道访问者模式是什么以及如何使用它;这个问题不是这个one的重复。


我有一个库,其中放置了我编写的大部分可重用代码,并链接到我的大多数项目。

我经常需要向某些类添加功能,但没有将这些新功能添加到库中。让我用一个真实的例子:

在这个库中,我有一个类Shape,由CircleShapePolygonShapeCompositeShape 继承。

我现在正在开发一个图形应用程序,我需要在其中渲染这些 Shape,但不想将虚函数 render 放在核心 Shape 类中,因为我的一些项目使用 @ 987654329@ 不进行任何渲染,其他图形项目可以使用不同的渲染引擎(我在这个项目中使用 Qt,但对于游戏我会使用 OpenGL,因此 render 函数将需要不同的实现)。

当然,最有名的方法是使用访问者模式,但这让我产生了一些疑问:

任何库的任何类都可能需要像我的Shape 那样进行扩展。大多数公共图书馆(几乎全部)都不提供对访问者模式的任何支持;为什么?我为什么要这样做?

访问者模式是一种在 C++ 中模拟双重调度的方法。它不是 C++ 原生的,需要显式实现,使类接口更加复杂:我认为 applyVisitor 函数不应该与我的类函数处于同一级别,我认为这就像破坏抽象。

dynamic_cast 明确地向上转换Shape 更昂贵,但对我来说它看起来是一个更清洁的解决方案。


那么,我该怎么办?在我所有的库类中实现双重调度?如果提供Shape 的库不是我的,而是网上找到的一些 GPL 库怎么办?

【问题讨论】:

  • 访问者模式不是获得多次调度的方法。它需要多次分派,因此作为访问者模式的一部分,通常会向 C++ 程序员教授模拟多次分派的技术。
  • 您需要多次分派吗?也许 NVI 模式适合您的需求。 NVI 设计似乎可以提供帮助,但它需要非虚拟渲染和私有虚拟 doRender 类型的方法。所以也许是 shape 和 shapecircle 之间的一个抽象类,称为 shaperenderengine。然后 ShapeCircle 可以从 shape 或 shaperenderengine 派生.....

标签: c++ visitor-pattern double-dispatch


【解决方案1】:

首先:“访问者模式是一种在 C++ 中模拟双重调度的方法。” 嗯,这并不完全正确。实际上,双重分派是多重分派的一种形式,它是在 C++ 中模拟(缺失的)多方法的一种方式。


确定类层次上的操作应该通过添加虚函数还是添加访问者来实现通过添加类与添加操作的概率:

  • 如果类的数量不断变化比操作的数量更快,使用虚函数。那是因为添加一个类需要修改所有访问者。
  • 如果类数相对于操作数比较稳定使用访问者。这是因为添加虚函数需要更改层次结构中的所有类。

是的,许多图书馆没有访问者界面。
当我们只看上面的推理时,如果类的数量经常变化,这将是正确的。也就是说,如果一个库经常发布,并且不断添加新类,那么提供访问者界面就没有多大意义,因为每次新版本带来新类时,使用该库的每个人都需要调整所有访问者.因此,如果我们只看上面的推理,访问者界面似乎只有在 lib 的类层次结构中的类数量很少或从不改变时才有用。

但是,对于第 3 方库,还有另一个方面:通常,用户无法更改库中的类。也就是说,如果他们需要添加操作,他们可以做到这一点的唯一方法是添加访问者 - 如果库提供了钩子让他们插入它
因此,如果您正在编写一个库并且觉得用户应该能够向其中添加操作,那么您需要为他们提供一种将访问者插入您的库的方法 .

【讨论】:

  • 所以你认为我应该为我的图书馆内的访问者添加一些支持?你能告诉我任何使用访问者模式让我获得灵感的图书馆吗?我知道的唯一一个是 Boost::Variant,但这种情况与我的“正常”使用完全不同。
  • @peoro:我不知道,因为我不知道你的库。坦率地说,我想不出一个随访客一起提供的 C++ 库。我同意boost::variant 不是您的普通日常访客。很抱歉帮不上忙。
【解决方案2】:

在我看来,这不像是访问者模式。

我建议你有一个聚合Shape 对象的RenderableShape 类,然后为每个形状创建子类。 RenderableShape 会有一个虚拟的 render 方法。

如果你想支持多个渲染引擎,你可以有一个抽象绘图操作的RenderContext基类,每个渲染引擎都有子类,每个子类根据其渲染引擎实现绘图操作。然后,您可以让 RenderableShape::renderRenderContext 作为参数,并使用其抽象 API 对其进行绘制。

【讨论】:

  • 我的库中的一些函数返回指向Shape 的指针/引用。例如,我谈到的CompositeShape 有一组Shape*,可以是任何类型的。因此,即使我将在应用程序代码中分配的Shape 保留在包装类(RenderableShape)中,我也无法处理库函数返回的Shape*
  • 不能把返回的Shape包装一下吗?
  • 是的,但是把它包装成什么?我不知道它是 CircleShape 还是 PolygonShape... 需要某种向上转换,但是为什么还要打扰包装器呢?我可以一直使用它。
【解决方案3】:

所以有一个类 xxxShape,它在某种程度上包含“驱动”渲染的信息。对于可能是中心的圆,半径,对于正方形,一些角坐标或一些这样的。也许还有一些关于填充物和颜色的其他东西。

您不想/不能更新这些类以添加实际的渲染逻辑,我认为您不这样做的原因是有效的/不可避免的。

但大概,你有足够的类上的公共访问方法,让你获得“驾驶”信息,否则你注定要失败。

那么在这种情况下,为什么你不能只包装这些物品:

 CircleRenderer hasA Cicle, knows how to render Circles

等等。现在在 Renderer 类中使用访问者模式。

【讨论】:

  • 我的库中的一些函数返回指向Shape 的指针/引用。例如,我谈到的CompositeShape 有一组Shape*,可以是任何类型的。因此,即使我将在应用程序代码中分配的Shape 保留在包装类中,我也无法处理库函数返回的Shape*
【解决方案4】:

有许多可能的解决方案,但您可以这样做,例如:启动新的层次结构,在特定的 Context 中呈现 Shapes

// contracts:

class RenderingContext {
public: virtual void DrawLine(const Point&, const Point&) = 0; 
    // and so on...
};

class ShapeRenderer {
public: virtual void Render(RenderingContext&) = 0;
};

// implementations:

class RectangleRenderer : public ShapeRenderer {
 Rectangle& mR;

public: 
 virtual void Render(RenderingContext& pContext) {
   pContext.DrawLine(mR.GetLeftLower(), mR.GetRightLower());
   // and so on...
 }

 RectangleRenderer(Rectangle& pR) : mR(pR) {}
};

【讨论】:

  • 我的库中的一些函数返回指向Shape 的指针/引用。例如,我谈到的CompositeShape 有一组Shape*,可以是任何类型的。因此,即使我将在应用程序代码中分配的Shape 保留在包装类中,我也无法处理库函数返回的Shape*
【解决方案5】:

我完全理解你所说的,我也有同样的担忧。 问题是访问者模式的定义不是很明确,而且它的原始解决方案具有误导性,恕我直言。这就是为什么这种模式有这么多变体的原因。

特别是,我认为正确的实现应该支持遗留代码,我的意思是:一个二进制文件你已经完全丢失了源代码,不是吗?这就是定义所说的:您永远不必更改原始数据结构。

我不喜欢visitA、visitB、visitWhatever、acceptA、acceptB、acceptWhatever 的实现。这是绝对错误的,恕我直言。

如果有机会,请关注an article I've written about this

它是 Java,但如果您发现它对您的目的有用,您可以轻松地移植到 C++。

希望对你有帮助

干杯

【讨论】:

    猜你喜欢
    • 2014-02-03
    • 2012-03-11
    • 2015-11-24
    • 1970-01-01
    • 2011-07-25
    • 2013-12-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多