为什么C ++不允许您请求指向最派生类的指针?

时间:2010-06-16 14:15:32

标签: c++ inheritance rtti multiple-dispatch dynamic-dispatch

(这个问题应该通过对Stroustrup的引用来回答。)

能够请求指向最派生类的指针似乎非常有用,如下所示:

class Base { ... };
class DerivedA { ... };
class DerivedB { ... };
class Processor
{
  public:
  void Do(Base* b) {...}
  void Do(DerivedA* d) {...}
  void Do(DerivedB* d) {...}
};

list<Base*> things;
Processor p;
for(list<Base*>::iterator i=things.begin(), e=things.end(); i!=e; ++i)
{
    p.Do(CAST_TO_MOST_DERIVED_CLASS(*i));
}

但是c ++中没有提供这种机制。为什么呢?

更新,激励示例:

假设您没有Base和Derived and Processor,而是拥有:

class Fruit
class Apple : public Fruit
class Orange: public Fruit

class Eater
{
   void Eat(Fruit* f)  { ... }
   void Eat(Apple* f)  { Wash(f); ... }
   void Eat(Orange* f) { Peel(f); ... }
};

Eater me;
for each Fruit* f in Fruits
    me.Eat(f);

但这在C ++中很棘手,需要像访问者模式这样的创造性解决方案。那么,问题是:为什么在C ++中这样做很棘手,像“CAST_TO_MOST_DERIVED”这样的东西会让它变得更简单?

更新:维基百科全知

我认为Pontus Gagge有一个很好的答案。从Multiple Dispatch上的维基百科条目中添加它:

  

“Stroustrup提到他喜欢 C ++的设计和演变中的多方法概念,并考虑在C ++中实现它,但声称无法找到有效的示例实现(与虚拟函数)并解决了一些可能的类型模糊问题。他接着说,尽管该特性仍然很好,但它可以使用双调度或基于类型的查找表来近似实现,如C / C ++示例中所述。以上是未来语言修订的低优先级功能。“

对于背景知识,你可以阅读一些关于Multi-Methods的摘要,这比我提到的那个更好,因为它们只是起作用。

10 个答案:

答案 0 :(得分:8)

可能是因为这就是虚拟功能为您所做的事情。当您通过基类指针或引用调用它时,将调用最接近最派生类的虚函数的实现。

答案 1 :(得分:8)

首先,C ++允许您在数字术语中请求指向最派生类的指针(即只是地址的数值)。这是dynamic_castvoid*的作用。

其次,没有办法获得指向最派生类的精确类型的最派生类的指针。在C ++演员表中使用静态类型静态类型是编译时概念。基于类型的函数重载也是一个编译时进程。在您的情况下,在编译时不知道确切的派生类型,这就是为什么不能转换为它并且无法解决它的重载。在C ++语言领域,拥有这样的强制转换的请求毫无意义。

你想要实现的目标(如果我理解你的意图正确),是通过完全不同的方式实现的,而不是通过演员实现的。有关示例,请阅读 double dispatch

答案 2 :(得分:6)

因为i的类型在编译时无法确定。因此编译器不知道要生成哪个函数调用。 C ++只支持一种动态分派方法,即虚函数机制。

答案 3 :(得分:4)

使用虚函数调用调用它。将处理器*传递给DerivedA / B的虚拟方法。不是相反。

没有提供任何机制,因为它完全没有必要和冗余。

我发誓,我在一两天前提出了这个问题。

答案 4 :(得分:4)

您建议的内容相当于运行时类型上的switch,调用其中一个重载函数。正如其他人所指出的那样,您应该使用 继承层次结构,而不是反对:在类层次结构中使用虚拟而不是在其外部调度。

也就是说,这样的事情对double dispatch有用,特别是如果你的Processors层次结构也是如此。但是编译器将如何实现它呢?

首先,您必须在运行时提取您称为“最重载类型”的内容。它可以完成,但你会如何处理,例如多继承和模板?语言中的每个功能都必须与其他功能很好地交互 - 而C ++具有大量功能!

其次,为了使您的代码示例正常工作,您必须根据运行时类型(C ++在设计时不允许)获得正确的静态重载。您是否希望遵循compile time lookup rules,尤其是多个参数?您是否希望此运行时调度还考虑Processor层次结构的运行时类型以及它们添加了哪些重载?您希望编译器自动添加到运行时调度程序中的逻辑是多少?您将如何处理无效的运行时类型?该功能的用户是否会意识到简单的强制转换和函数调用的成本和复杂性?

总而言之,我说这个功能实现起来很复杂,在实现和使用上都容易出错,并且只在极少数情况下才有用。

答案 5 :(得分:2)

在C ++中,重载解析在编译时发生。您的示例需要在运行时确定*i的实际类型。要在运行时完成它需要运行时类型检查,并且因为C ++是一种面向性能的语言,所以它有目的地避免了这种成本。如果你真的想要这样做(我很想看到一个更现实的例子)你可以dynamic_cast到最派生的类,那么如果它失败到第二个派生类,依此类推,但这需要知道前面的类层次结构。并且预先了解完整的层次结构可能是不可能的 - 如果DerivedB类在公共头中,则可能另一个库使用它并且已经创建了更多的派生类。

答案 6 :(得分:2)

您正在寻找double dispatch。它可以在C ++中完成,如该链接所示,但它并不漂亮,它基本上涉及使用两个相互调用的虚函数。如果您无法修改继承树中的某些对象,则可能无法使用此技术。

答案 7 :(得分:1)

这在C ++中是不可能的,但使用Visitor design pattern可以轻松实现您想要实现的目标:

class Base
{
    virtual void accept(BaseVisitor& visitor) { visitor.visit(this); }
};

class DerivedA
{
    virtual void accept(BaseVisitor& visitor) { visitor.visit(this); }
};

class DerivedB
{
    virtual void accept(BaseVisitor& visitor) { visitor.visit(this); }
};

class BaseVisitor
{   
    virtual void visit(Base* b) = 0;
    virtual void visit(DerivedA* d) = 0;
    virtual void visit(DerivedB* d) = 0;
};

class Processor : public BaseVisitor
{
    virtual void visit(Base* b) { ... }
    virtual void visit(DerivedA* d) { ... }
    virtual void visit(DerivedB* d) { ... }
};

list<Base*> things;
Processor p;
for(list<Base*>::iterator i=things.begin(), e=things.end(); i!=e; ++i)
{
    (*i)->visit(p);
}

答案 8 :(得分:1)

为什么C ++没有它?也许创作者从未想过它。或许他们认为它不合适或不够有用。或者也许在用这种语言尝试这样做时会出现问题。

关于最后一种可能性,这是一个思想实验:

让我们说这个特性存在,以便编译器编写代码来检查指向的动态类型并调用适当的重载。现在我们还要说代码的一部分有class DerivedC : Base {...};。并且说相应的Processor::Do重载是而不是

鉴于所有这些,程序在尝试选择适当的重载时应该怎么做?这种差异无法在编译时捕获。它应该尝试爬上类层次结构来查找与基类匹配的函数吗?它应该抛出一个特殊的例外吗?应该崩溃吗?还有其他可能吗?在不知道代码和类层次结构的意图的情况下,编译器可以自己做出任何合理的选择吗?

是的,自己编写这样的功能会容易出现同样的问题,但程序员可以完全控制选择行为,而不是编译器。

答案 9 :(得分:0)

C ++解释相关类型的上下文中的数据。在列表中存储DerivedA *或DerivedB *的实例时,该关联类型必须是Base *。这意味着编译器本身不再能够确定那些是指向其中一个子类而不是基类的指针。虽然理论上你可以通过查看相关类型的继承来转换为LESS派生类,但是在编译时无法获得所需的信息。