简体   繁体   English

避免 dynamic_cast 的模式

[英]pattern to avoid dynamic_cast

I have a class:我有一堂课:

class A 
{
public:
  virtual void func() {…}
  virtual void func2() {…}
};

And some derived classes from this one, lets say B,C,D... In 95 % of the cases, i want to go through all objects and call func or func2(), so therefore i have them in a vector, like:还有一些派生类,比如 B、C、D……在 95% 的情况下,我想遍历所有对象并调用 func 或 func2(),因此我将它们放在一个向量中,例如:

std::vector<std::shared_ptr<A> > myVec;
…
for (auto it = myVec.begin(); it != myVec.end(); ++it)
  (*it).func();

However, in the rest 5 % of the cases i want to do something different to the classes depending on their subclass.但是,在其余 5% 的情况下,我想根据它们的子类对类做一些不同的事情。 And I mean totally different, like calling functions that takes other parameters or not calling functions at all for some subclasses.我的意思是完全不同的,比如调用带有其他参数的函数或根本不调用某些子类的函数。 I have thought of some options to solve this, none of which I really like:我想了一些解决这个问题的选项,但我都不喜欢:

  • Use dynamic_cast to analyze subclass.使用 dynamic_cast 来分析子类。 Not good, too slow as I make calls very often and on limited hardware不好,太慢了,因为我经常打电话而且硬件有限

  • Use a flag in each subclass, like an enum {IS_SUBCLASS_B, IS_SUBCLASS_C}.在每个子类中使用一个标志,例如枚举 {IS_SUBCLASS_B, IS_SUBCLASS_C}。 Not good as it doesnt feel OO.不好,因为它不觉得 OO。

  • Also put the classes in other vectors, each for their specific task.还将这些类放在其他向量中,每个向量都用于其特定任务。 This doesnt feel really OO either, but maybe I'm wrong here.这也不是真的 OO,但也许我在这里错了。 Like:喜欢:

     std::vector<std::shared_ptr<B> > vecForDoingSpecificOperation; std::vector<std::shared_ptr<C> > vecForDoingAnotherSpecificOperation;

So, can someone suggest a style/pattern that achieves what I want?那么,有人可以建议一种实现我想要的风格/模式吗?

Someone intelligent (unfortunately I forgot who) once said about OOP in C++: The only reason for switch -ing over types (which is what all your suggestions propose) is fear of virtual functions.一个聪明的人(不幸的是我忘了是谁)曾经说过 C++ 中的 OOP: switch类型的唯一原因(这是你所有的建议提出的)是对虚函数的恐惧。 (That's para-paraphrasing.) Add virtual functions to your base class which derived classes can override, and you're set. (这是对解释的意思。)将虚函数添加到派生类可以覆盖的基类,然后就设置好了。
Now, I know there are cases where this is hard or unwieldy.现在,我知道有些情况下这很难或笨拙。 For that we have the visitor pattern.为此,我们有访问者模式。

There's cases where one is better, and cases where the other is.有些情况下一个更好,也有另一个更好的情况。 Usually, the rule of thumb goes like this:通常,经验法则是这样的:

  • If you have a rather fixed set of operations , but keep adding types , use virtual functions .如果您有一组相当固定的操作,但不断添加类型,请使用虚函数
    Operations are hard to add to/remove from a big inheritance hierarchy, but new types are easy to add by simply having them override the appropriate virtual functions.操作很难在大的继承层次结构中添加/删除,但只需让它们覆盖适当的虚函数,就可以轻松添加新类型。

  • If you have a rather fixed set of types , but keep adding operations , use the visitor pattern .如果您有一组相当固定的类型,但不断添加操作,请使用访问者模式
    Adding new types to a large set of visitors is a serious pain in the neck, but adding a new visitor to a fixed set of types is easy.向大量访问者添加新类型是一件令人头疼的事,但向固定类型集添加新访问者很容易。

(If both change, you're doomed either way.) (如果两者都改变​​,你注定要失败。)

According to your comments, what you have stumbled upon is known (dubiously) as the Expression Problem , as expressed by Philip Wadler:根据您的评论,您偶然发现的内容(令人怀疑)被称为表达问题,正如 Philip Wadler 所说:

The Expression Problem is a new name for an old problem.表达式问题是旧问题的新名称。 The goal is to define a datatype by cases, where one can add new cases to the datatype and new functions over the datatype, without recompiling existing code, and while retaining static type safety (eg, no casts).目标是按案例定义数据类型,其中可以向数据类型添加新案例并在数据类型上添加新函数,而无需重新编译现有代码,同时保留静态类型安全(例如,无强制转换)。

That is, extending both "vertically" (adding types to the hierarchy) and "horizontally" (adding functions to be overriden to the base class) is hard on the programmer.也就是说,“垂直”(将类型添加到层次结构)和“水平”(添加要覆盖到基类的函数)对程序员来说很难

There was a long (as always) discussion about it on Reddit in which I proposed a solution in C++ .在 Reddit 上有很长时间(一如既往)关于它的讨论,我在其中提出了一个C++ 解决方案

It is a bridge between OO (great at adding new types) and generic programming (great at adding new functions).它是 OO(擅长添加新类型)和泛型编程(擅长添加新功能)之间的桥梁。 The idea is to have a hierachy of pure interfaces and a set of non-polymorphic types.这个想法是拥有一个纯接口的层次结构和一组非多态类型。 Free-functions are defined on the concrete types as needed, and the bridge with the pure interfaces is brought by a single template class for each interface (supplemented by a template function for automatic deduction).自由函数根据需要定义在具体类型上,与纯接口的桥梁由每个接口的单个​​模板类带来(补充一个模板函数用于自动推导)。

I have found a single limitation to date: if a function returns a Base interface, it may have been generated as-is, even though the actual type wrapped supports more operations, now.迄今为止,我发现了一个限制:如果一个函数返回一个Base接口,它可能是按原样生成的,即使包装的实际类型现在支持更多操作。 This is typical of a modular design (the new functions were not available at the call site).这是典型的模块化设计(新功能在呼叫站点不可用)。 I think it illustrates a clean design, however I understand one could want to "recast" it to a more verbose interface.我认为它说明了一个干净的设计,但是我理解人们可能希望将它“重铸”为更详细的界面。 Go can, with language support (basically, runtime introspection of the available methods). Go可以,具有语言支持(基本上,可用方法的运行时内省)。 I don't want to code this in C++.我不想用C编写这个++。


As already explained myself on reddit... I'll just reproduce and tweak the code I already submitted there.正如我在 reddit 上已经解释过的那样......我将复制和调整我已经在那里提交的代码。

So, let's start with 2 types and a single operation.因此,让我们从 2 种类型和单个操作开始。

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

struct Circle { double radius; };
double area(Circle const c);

Now, let's make a Shape interface:现在,让我们制作一个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)); }

Okay, C++ is verbose.好吧,C++ 是冗长的。 Let's check the use immediately:让我们立即检查使用情况:

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";
}

So, first exercise, let's add a shape (yep, it's all):所以,第一个练习,让我们添加一个形状(是的,就是这样):

struct Rectangle { double length, height; };
double area(Rectangle const r);

Okay, so far so good, let's add a new function.好的,到目前为止一切顺利,让我们添加一个新功能。 We have two options.我们有两个选择。

The first is to modify Shape if it is in our power.第一个是修改Shape如果它在我们的能力范围内。 This is source compatible, but not binary compatible.这是源兼容的,但不是二进制兼容的。

// 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);

It may seem that we fall into the Expression Problem here, but we don't.看起来我们在这里陷入了表达问题,但我们没有。 We needed to add the perimeter for each (already known) class because there is no way to automatically infer it;我们需要为每个(已知的)类添加周长,因为无法自动推断它; however it did not require editing each class either!但是它也不需要编辑每个类!

Therefore, the combination of External Interface and free functions let us neatly (well, it is C++...) sidestep the issue.因此,外部接口和自由函数的结合让我们巧妙地(好吧,它是 C++...)回避了这个问题。

sodraz noticed in comments that the addition of a function touched the original interface which may need to be frozen (provided by a 3rd party, or for binary compatibility issues). sodraz在评论中注意到添加一个函数触及了可能需要冻结的原始界面(由第三方提供,或用于二进制兼容性问题)。

The second options therefore is not intrusive, at the cost of being slightly more verbose:因此,第二个选项不是侵入性的,代价是稍微更冗长:

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)); }

And then, define the perimeter function for all those Shape we would like to use with the ExtendedShape .然后,为所有我们想与ExtendedShape一起使用的Shape定义perimeter函数。

The old code, compiled to work against Shape , still works.旧的代码,编译为对Shape工作,仍然有效。 It does not need the new function anyway.反正它不需要新功能。

The new code can make use of the new functionality, and still interface painlessly with the old code.新代码可以利用新功能,并且仍然可以轻松地与旧代码交互。 (*) (*)

There is only one slight issue, if the old code return a ShapePtr , we do not know whether the shape actually has a perimeter function (note: if the pointer is generated internally, it has not been generated with the newExtendedShape mechanism).只有一个小问题,如果旧代码返回一个ShapePtr ,我们不知道这个形状是否真的有一个周长函数(注意:如果指针是内部生成的,它没有用newExtendedShape机制生成)。 This is the limitation of the design mentioned at the beginning.这就是开头提到的设计的局限性 Oops :)哎呀:)

(*) Note: painlessly implies that you know who the owner is. (*) 注意:无痛暗示您知道所有者是谁。 A std::unique_ptr<Derived>& and a std::unique_ptr<Base>& are not compatible, however a std::unique_ptr<Base> can be build from a std::unique_ptr<Derived> and a Base* from a Derived* so make sure your functions are clean ownership-wise and you're golden. std::unique_ptr<Derived>&std::unique_ptr<Base>&不兼容,但是std::unique_ptr<Base>可以从std::unique_ptr<Derived>Base*构建Derived*因此请确保您的功能在所有权方面是干净的,并且您是黄金。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM