如何避免在C++代码中使用dynamic_cast?

18

假设我有以下的类结构:

class Car;
class FooCar : public Car;
class BarCar : public Car;

class Engine;
class FooEngine : public Engine;
class BarEngine : public Engine;
让我们给一个名为Car的类添加对其Engine的引用。创建一个具有FooEngine*FooCar和一个具有BarEngine*BarCar。是否有一种方式可以安排事情,使得FooCar对象可以调用FooEngine的成员函数而无需进行向下转换?
目前这个类结构的布局原因如下:
1.所有的Car都有一个Engine。此外,FooCar只会使用FooEngine
2.有一些数据和算法是所有Engine共享的,我不想复制和粘贴它们。
3.可能会编写需要一个Engine知道其Car的函数。
在编写此代码时,一旦键入dynamic_cast,我就知道自己可能做错了什么。是否有更好的方法来解决这个问题?
更新:
根据迄今为止给出的答案,我倾向于两种可能性:
1.让Car提供一个纯虚拟的getEngine()函数。这将允许FooCarBarCar具有返回正确类型的Engine的实现。
2.将所有Engine功能吸收到Car继承树中。出于维护原因,Engine被拆分开来(以将Engine的东西保留在一个单独的位置)。这是在代码行数上拥有更多小类(小类)与拥有更少大类之间的权衡。
是否有强烈的社区偏好选项?是否有我还没有考虑的第三种选择?

也许你应该将问题的标题具体化一些?比如“如何避免使用dynamic_cast”。顺便说一句,这是一个非常好的问题。 - David Holm
10个回答

24

我假设Car持有一个引擎指针,这就是你发现自己需要向下转型的原因。

从基类中取出指针,并替换为纯虚函数get_engine()。然后你的FooCar和BarCar可以持有指向正确引擎类型的指针。

(编辑)

这样做的原因:

由于虚函数Car::get_engine()会返回引用或指针,C++将允许派生类使用不同的返回类型来实现此函数,只要返回类型仅因为是更多的派生类型而不同。

这被称为covariant return types,并将允许每个Car类型返回正确的Engine。


5
由于虚函数允许协变返回类型,你可以使返回值成为适当的FooEngine或BarEngine类型!这个简单的解决方案是我忽略了的加1分。 - rmeador
1
这是正确的方法。@Kristo,试试看。 - Daniel Daranas
在我看来,这只是把向下转型问题推到了下一个阶段。 - cletus
1
这样做如何解决向下转型问题? - Michael Kristofik
1
Kristo:查找协变返回类型,你就会明白。关键是不要指定在超类中实际存储引擎的成员变量——只使用这个抽象的访问器,在子类中重写以使用相应类型的成员变量。 - rmeador
1
@rmeador:我理解你的回答。我应该在我的评论中标记“@cletus”。 - Michael Kristofik

10

我想补充一件事:这个设计已经因为我所谓的并行树而让我感到糟糕。

基本上,如果你最终得到了平行的类层次结构(就像车和引擎那样),那么你只是在招惹麻烦。

我会重新考虑引擎(甚至汽车)是否需要有子类,或者它们是否只是同一个基类的不同实例。


2
你能提供一个具体的例子(基于我上面的类结构),展示如何解决“并行树”问题吗? - Michael Kristofik
1
我也对“正确”的解决方案感兴趣。我总是不喜欢那些并行树,但无法真正解释为什么或者更重要的是提出更好的解决方案。 - Franz

8
您可以将引擎类型模板化,如下所示。
template<class EngineType>
class Car
{
    protected:
        EngineType* getEngine() {return pEngine;}
    private:
        EngineType* pEngine;
};

class FooCar : public Car<FooEngine>

class BarCar : public Car<BarEngine>

如果我理解正确的话,这基本上相当于建议一个纯虚拟的getEngine函数的答案。这是编译时多态性与运行时多态性之间的区别,对吧? - Michael Kristofik
1
是的,除了这个有点更加严格,因为它要求你实际上持有一个指针,而对于纯虚函数,子类可以自由地根据自己的意愿来实现它。 - JohnMcG
1
这种方法的一个限制是,您不能再将FooCar作为Car&Car *传递 - 如果您想对其进行任何与Car通用的操作,则必须将其传递到模板函数/方法/类型中。 - mskfisher
1
没错 - 这是编译时多态性,而不是运行时。 - JohnMcG
1
@JohnMcG +1 多态 :) - rfcoder89

5

我认为汽车完全可以由发动机组成(只要BarCar始终包含BarEngine)。发动机与汽车有着非常紧密的关系。

我的偏好是:

class BarCar:public Car
{
   //.....
   private:
     BarEngine engine;
}

1
我认为这对我不起作用,因为Car基类需要与其引擎进行交互。 - Michael Kristofik
当然,有什么阻止Car的私有方法调用Engine的公共方法呢?除非你想进一步抽象它..即Car类首先包含Engine指针。 - Sridhar Iyer

2

FooCar是否能使用BarEngine?

如果不行,你可能需要使用抽象工厂来创建正确的汽车对象和引擎。


如果我理解 OP 的问题,那并不能帮助他,因为他头疼的是类型为 Engine 的继承“engine”成员变量... 他希望它是协变的,但我认为在 C++ 中不允许这样做,所以我会避免发布一个无关的答案... - rmeador

2

您可以将FooEngine存储在FooCar中,将BarEngine存储在BarCar中。

class Car {
public:
  ...
  virtual Engine* getEngine() = 0;
  // maybe add const-variant
};

class FooCar : public Car
{
  FooEngine* engine;
public:
  FooCar(FooEngine* e) : engine(e) {}
  FooEngine* getEngine() { return engine; }
};

// BarCar similarly

这种方法的问题在于获取引擎是一个虚函数调用(如果您关心这一点),而在Car中设置引擎的方法需要向下转型。

我支持你的答案。但是我有一个问题:为什么要“设置”FooCar的FooEngine?汽车构造函数应该也建造汽车自己的引擎:FooCar(){engine = new FooEngine();} Engine *getEngine(){return engine;} - Federico A. Ramponi
完全取决于 OP 想要做什么。真实汽车的引擎可以更换,甚至还有合法的程序。因此,如果他想建模,就必须进行一些 setEngine() 方法的操作。 - jpalecek
在这种情况下,汽车的引擎将永远不会更换。 - Michael Kristofik

1

我认为这取决于 Engine 是仅由 Car 及其子类私下使用,还是您想在其他对象中使用它。

如果 Engine 的功能不特定于 Car,那么我会使用一个 virtual Engine* getEngine() 方法来替代在基类中保留指针。

如果它的逻辑是特定于 Car 的,我更喜欢将通用的 Engine 数据/逻辑放在一个单独的对象中(不一定是多态的),并将 FooEngineBarEngine 实现放在其各自的 Car 子类中。

当需要更多地进行实现重用而非接口继承时,对象组合通常提供了更大的灵活性。


1

微软的COM有点笨重,但它确实有一个新颖的概念——如果你有一个对象接口的指针,你可以使用QueryInterface函数查询它是否支持任何其他接口。这个想法是将你的引擎类分解成多个接口,以便每个接口都可以独立使用。


0

如果我没有漏掉什么,这应该是相当简单的。

最好的方法是在引擎中创建纯虚函数,然后要求在要实例化的派生类中使用这些函数。

额外的加分解决方案可能是创建一个名为IEngine的接口,然后将其传递给您的汽车,IEngine中的每个函数都是纯虚函数。您可以有一个“BaseEngine”来实现您想要的一些功能(即“共享”),然后从中派生出叶子类。

接口很好,如果您有任何想要“看起来像引擎”的东西,但实际上并不是(即模拟测试类等)。


0
有没有一种方法可以安排事情,使得 FooCar 对象可以调用 FooEngine 的成员函数而不需要向下转换? 像这样:
class Car
{
  Engine* m_engine;
protected:
  Car(Engine* engine)
  : m_engine(engine)
  {}
};

class FooCar : public Car
{
  FooEngine* m_fooEngine;
public:
  FooCar(FooEngine* fooEngine)
  : base(fooEngine)
  , m_fooEngine(fooEngine)
  {}
};

我可以看出这个设计会引起各种问题。如果Car定义了一个void setEngine(Engine* engine)方法,那怎么办?外部客户端可能会在FooCar上调用它,现在你的对象完整性已经丢失了。 - maxaposteriori
如果Car定义了一个“void setEngine(Engine* engine)”方法,那么这个方法需要是受保护的或者是抽象的。或者,Car中替代拥有“Engine* m_engine”的另一种方式是在Car中拥有一个私有的抽象“Engine* get_engine()”方法。 - ChrisW
在我的情况下不会有setEngine函数,但我同意这可能会导致维护问题。对于Car子类来说,拥有两个(类型不同!)指向相同引擎的句柄是令人困惑的。 - Michael Kristofik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接