模式,以避免dynamic_cast

Fra*_*ank 18 c++ oop c++11

我有一节课:

class A 
{
public:
  virtual void func() {...}
  virtual void func2() {...}
};
Run Code Online (Sandbox Code Playgroud)

还有一些来自这个的派生类,比方说B,C,D ...在95%的情况下,我想要遍历所有对象并调用func或func2(),因此我将它们放在向量中,如:

std::vector<std::shared_ptr<A> > myVec;
...
for (auto it = myVec.begin(); it != myVec.end(); ++it)
  (*it).func();
Run Code Online (Sandbox Code Playgroud)

但是,在剩下的5%的情况下,我想根据它们的子类做一些不同的类.我的意思完全不同,比如调用带有其他参数的函数或者根本不为某些子类调用函数.我想到了一些解决这个问题的方法,我都不喜欢这个方案:

  • 使用dynamic_cast来分析子类.不好,太慢,因为我经常在有限的硬件上打电话
  • 在每个子类中使用一个标志,如枚举{IS_SUBCLASS_B,IS_SUBCLASS_C}.不好,因为它没有感觉到OO.
  • 还将类放在其他向量中,每个向量用于其特定任务.这也不觉得OO,但也许我错了.喜欢:

    std::vector<std::shared_ptr<B> > vecForDoingSpecificOperation;
    std::vector<std::shared_ptr<C> > vecForDoingAnotherSpecificOperation;
    
    Run Code Online (Sandbox Code Playgroud)

那么,有人可以建议一种能达到我想要的风格/模式吗?

sbi*_*sbi 35

有人聪明(不幸的是我忘了谁)曾经在C++中谈到过OOP:对于switch类型(这是你所有的建议提出的)的唯一原因是对虚函数的恐惧.(这是一种解释.)将虚函数添加到基类中,派生类可以覆盖这些函数,并进行设置.
现在,我知道有些情况下这很难或很笨拙.为此,我们有访客模式.

在某些情况下,一个更好,而另一个则更好.通常,经验法则是这样的:

  • 如果您有一组相当固定的操作,但继续添加类型,请使用虚函数.
    操作很难在大型继承层次结构中添加/删除,但只需让它们覆盖适当的虚函数即可轻松添加新类型.

  • 如果您有一组相当固定的类型,但继续添加操作,请使用访问者模式.
    向大量访问者添加新类型是一个严重的问题,但是为一组固定类型添加新访问者很容易.

(如果两者都改变​​了,那你就注定了.)

  • 如果没有[Boost.Variant](http://www.boost.org/libs/variant/)的链接,就不应该提及访客模式.: - ] (4认同)
  • @sbi:这是臭名昭着的(并且可疑的名称)[表达式问题](http://en.wikipedia.org/wiki/Expression_problem). (2认同)
  • @MatthieuM。这并不是可疑的:它是在您设计语言解释器/编译器/任何东西,并且不断向AST中添加表达式类型时出现的。在Haskell中,有一种使用代数技巧(代数的“自由monad”)和非常灵活的类型系统来减轻它的方法。例如。http://www.cs.ru.nl/~W.Swierstra/Publications/DataTypesALaCarte.pdf。确实,当类型不变(或稍有变化)时,最好使用功能语言而不是C ++ (2认同)
  • @MatthieuM.我非常同意,因为在实践中,它并不像"类型不变"或"行为不变"那样明确.无论如何,我发现很少有语言能够为这种复杂情况提供支持(性能合理).C++是一个,Haskell是另一个.当类型从一开始(甚至粗略地)修复时,代数数据类型+模式匹配比OOP和访问者模式更容易使用.但同样,您可能无法选择语言. (2认同)

Mat*_* M. 7

根据你的评论,正如菲利普·瓦德勒所表达的那样,你偶然发现的是(可疑地)已知的表达问题:

表达式问题是旧问题的新名称.目标是按案例定义数据类型,其中可以在数据类型上添加新案例,在数据类型上添加新函数,而无需重新编译现有代码,同时保留静态类型安全性(例如,无强制转换).

也就是说,对程序员来说,"垂直"(向层次结构添加类型)和"水平"(向函数添加要覆盖的函数)都很难.

在Reddit上有一个很长的(一如既往)关于它的讨论,我在其中提出了一个C++解决方案.

它是OO(非常适合添加新类型)和泛型编程(非常适合添加新功能)之间的桥梁.我们的想法是拥有纯粹的接口和一组非多态类型的层次结构.根据需要在具体类型上定义自由函数,并且具有纯接口的桥由每个接口的单个​​模板类引入(由用于自动演绎的模板函数补充).

到目前为止,我发现了一个限制:如果一个函数返回一个Base接口,它可能是按原样生成的,即使实际的包装类型现在支持更多的操作.这是典型的模块化设计(呼叫站点无法使用新功能).我认为它说明了一个干净的设计,但是我知道有人可能想要将它"重铸"到更详细的界面.Go可以,语言支持(基本上,可用方法的运行时内省).我不想编写在C++中.


正如我已经在reddit上解释的那样......我只是重现并调整我已在那里提交的代码.

那么,让我们从2种类型和单个操作开始.

struct Square { double side; };
double area(Square const s);

struct Circle { double radius; };
double area(Circle const c);
Run Code Online (Sandbox Code Playgroud)

现在,让我们创建一个Shape界面:

class Shape {
public:
   virtual ~Shape();

   virtual double area() const = 0;

protected:
   Shape(Shape const&) {}
   Shape& operator=(Shape const&) { return *this; }
};

typedef std::unique_ptr<Shape> ShapePtr;

template <typename T>
class ShapeT: public Shape {
public:
   explicit ShapeT(T const t): _shape(t) {}

   virtual double area() const { return area(_shape); }

private:
  T _shape;
};

template <typename T>
ShapePtr newShape(T t) { return ShapePtr(new ShapeT<T>(t)); }
Run Code Online (Sandbox Code Playgroud)

好的,C++很冗长.我们马上检查一下用法:

double totalArea(std::vector<ShapePtr> const& shapes) {
   double total = 0.0;
   for (ShapePtr const& s: shapes) { total += s->area(); }
   return total;
}

int main() {
  std::vector<ShapePtr> shapes{ new_shape<Square>({5.0}), new_shape<Circle>({3.0}) };

  std::cout << totalArea(shapes) << "\n";
}
Run Code Online (Sandbox Code Playgroud)

所以,首先练习,让我们添加一个形状(是的,这就是全部):

struct Rectangle { double length, height; };
double area(Rectangle const r);
Run Code Online (Sandbox Code Playgroud)

好的,到目前为止一切顺利,让我们添加一个新功能.我们有两种选择.

第一个是修改Shape它是否在我们的权力.这是源兼容的,但不兼容二进制.

// 1. We need to extend Shape:
  virtual double perimeter() const = 0

// 2. And its adapter: ShapeT
  virtual double perimeter() const { return perimeter(_shape); }

// 3. And provide the method for each Shape (obviously)
double perimeter(Square const s);
double perimeter(Circle const c);
double perimeter(Rectangle const r);
Run Code Online (Sandbox Code Playgroud)

看起来我们可能会陷入表达问题,但我们却没有.我们需要为每个(已知的)类添加周长,因为无法自动推断它; 但是它也不需要编辑每个类!

因此,外部接口和自由函数的结合让我们整齐地(好吧,它是C++ ......)回避了这个问题.

sodraz 在评论中注意到功能的添加触及了原始界面,可能需要冻结(由第三方提供,或者用于二进制兼容性问题).

因此,第二种选择并不是侵入性的,代价是稍微冗长:

class ExtendedShape: public Shape {
public:
  virtual double perimeter() const = 0;
protected:
  ExtendedShape(ExtendedShape const&) {}
  ExtendedShape& operator=(ExtendedShape const&) { return *this; }
};

typedef std::unique_ptr<ExtendedShape> ExtendedShapePtr;

template <typename T>
class ExtendedShapeT: public ExtendedShape {
public:
   virtual double area() const { return area(_data); }
   virtual double perimeter() const { return perimeter(_data); }
private:
  T _data;
};

template <typename T>
ExtendedShapePtr newExtendedShape(T t) { return ExtendedShapePtr(new ExtendedShapeT<T>(t)); }
Run Code Online (Sandbox Code Playgroud)

然后,perimeterShape我们想要使用的所有人定义函数ExtendedShape.

编译为反对的旧代码Shape仍然有效.无论如何它不需要新功能.

新代码可以使用新功能,并且仍然可以轻松地与旧代码接口.(*)

只有一个小问题,如果旧代码返回a ShapePtr,我们不知道该形状是否实际上具有周边函数(注意:如果指针是在内部生成的,则它尚未使用该newExtendedShape机制生成).这是开头提到的设计的限制.哎呀:)

(*)注意:无痛地暗示您知道拥有者是谁.一个std::unique_ptr<Derived>&std::unique_ptr<Base>&不兼容,但是一个std::unique_ptr<Base>可以从建立std::unique_ptr<Derived>Base*从一Derived*所以一定要确保你的职责是干净的所有权,聪明,你是金色的.