模式,以避免dynamic_cast

时间:2012-03-30 19:12:03

标签: c++ oop c++11

我有一个班级:

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

从这一个派生类中,我们可以说B,C,D ...在95%的情况下,我想要遍历所有对象并调用func或func2(),因此我将它们放在一个矢量,像:

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

然而,在剩下的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;
    

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

2 个答案:

答案 0 :(得分:32)

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

有一种情况更好,另一种情况则更好。通常,经验法则是这样的:

  • 如果你有一个相当 固定的操作集 ,但保持 添加类型 ,请使用 虚拟功能
    操作很难在大型继承层次结构中添加/删除,但只需让它们覆盖适当的虚函数即可轻松添加新类型。

  • 如果你有一个相当 固定的类型 ,但保持 添加操作 ,请使用 visitor pattern
    向大量访问者添加新类型是一个严重的问题,但是为一组固定类型添加新访问者很容易。

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

答案 1 :(得分:6)

根据你的评论,你所偶然发现的东西(可疑地)被称为Expression Problem,正如Philip Wadler所表达的那样:

  

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

也就是说,在程序员身上“垂直”(向层次结构中添加类型)和“水平”(向函数添加要覆盖的函数)都是 hard

在Reddit上进行了很长时间(一如既往)的讨论,我在其中提出了solution in C++

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

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


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

所以,让我们从2种类型和单一操作开始。

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

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

现在,让我们创建一个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)); }

好的,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";
}

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

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

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

首先是修改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);

似乎我们在这里陷入了表达问题,但我们却没有。我们需要为每个(已知的)类添加周长,因为无法自动推断它;但是它也不需要编辑每个类!

因此,外部接口和自由函数的结合让我们整齐地(好吧,它是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)); }

然后,为我们希望与perimeter一起使用的所有Shape定义ExtendedShape函数。

编译为针对Shape工作的旧代码仍然有效。无论如何它都不需要新功能。

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

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

(*)注意:无痛地暗示您知道所有者是谁。 std::unique_ptr<Derived>&std::unique_ptr<Base>&不兼容,但std::unique_ptr<Base>可以std::unique_ptr<Derived>Base* Derived*来构建{{1}}确保你的职能在所有权方面是清洁的,你是金色的。