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

Posted

技术标签:

【中文标题】为啥 C++ 不允许您请求指向最派生类的指针?【英文标题】:Why doesn't C++ allow you to request a pointer to the most derived class?为什么 C++ 不允许您请求指向最派生类的指针? 【发布时间】:2011-03-04 12:00:53 【问题描述】:

(这个问题应该可以参考 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 和 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 提到他喜欢The Design and Evolution of C++ 中的Multi-methods 概念,并考虑在C++ 中实现它,但声称无法找到有效的示例实现(与虚函数)并解决一些可能的类型歧义问题。他接着说,虽然这个特性仍然很好,但它可以使用双重分派或 C/C++ 示例中概述的基于类型的查找表来近似实现以上是未来语言修订的低优先级功能。”

作为背景,您可以阅读关于Multi-Methods 的简短摘要,这比我提到的那种电话要好,因为它们会起作用。

【问题讨论】:

我以前见过一些破解设计,但是......哦,伙计......Base::Do()DerivedA::Do()DerivedB::Do(),问题就消失了。 但有时这不是一个选项,或者在语义上它不是最佳选择。请参阅下面对 DeadMG 的回复。 【参考方案1】:

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

【讨论】:

其实虚函数不会为你做这些,因为虚函数必须在 Base/Derived 类中,这与函数 /not/ 在 Base/ 中是不一样的派生类。我将用一个激励性的例子进行编辑。 @Matthew Lowe:您的问题是“但是 C++ 中没有提供这种机制。为什么?”其中“this”是缺少对最派生类型的强制转换。虚拟功能允许您通过访问者模式之类的东西来实现您想要的。显然,您必须以与您希望的方式不同的方式设计事物,但它们可以为您解决问题。顺便说一句,我回答了您提出的问题,与您对@Ponuts Gagge 回答的评论中的含义形成鲜明对比。【参考方案2】:

首先,C++ 确实允许您以数字 术语(即地址的数值)请求指向派生类最多的类的指针。这就是 dynamic_castvoid* 所做的。

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

您尝试实现的内容(如果我正确理解您的意图)是通过完全不同的方式实现的,而不是通过演员表实现的。例如,阅读 双重调度

【讨论】:

dynamic_castvoid* ?真的吗?我的印象是你不能那样做。当然,有可能您将指针类型更改为其他类型之后,然后动态转换将起作用.. @ianmac45: 否。根据定义,dynamic_cast&lt;void *&gt;(p)(其中p 是一个指向多态类型的指针)计算为指向最派生对象的指针。当然,只是在数字上如此。结果类型仍然只是void * 请注意,我的问题不是“你能在 C++ 中做到这一点”,而是“为什么你不能在 C++ 中做到这一点”。我认为 Pontus Gagge 很好地回答了这个问题。 @Matthew Lowe:我回答的第二段(“其次...”)正是“为什么”问题的答案。【参考方案3】:

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

【讨论】:

相反!我可以多次调用 dynamic_cast 并实际完成工作。问题是,“为什么我不能只要求最派生的类呢?” 您可以动态转换,但生成的函数调用不会是动态的。 Do( base*) 将始终调用采用 base 参数等的函数。如果强制转换为大多数派生工作,应该生成哪个非动态函数调用?唯一适用于树中所有类的将是基类版本。如果你想进行动态函数调用,你只有一个工具可以做到。 我认为我没有正确传达我的意图。我所说的“转换为大多数派生类”的意思是,如果对象实际上是 DerivedB,我希望 CAST_TO_MOST_DERIVED_CLASS(&object) 返回 DerivedB*。如果是 DerivedC,我想要一个 DerivedC*。【参考方案4】:

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

也就是说,类似这样的内容可能对double dispatch 很有用,尤其是如果您还有Processors 的层次结构。但是编译器将如何实现它呢?

首先,您必须在运行时提取所谓的“最重载类型”。可以做到,但是您将如何处理,例如多重继承和模板?语言中的每个特性都必须与其他特性很好地交互——而 C++ 有很多特性!

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

总之,我想说该功能实现起来很复杂,在实现和使用中都容易出错,并且仅在极少数情况下有用。

【讨论】:

双重派送不需要这个。正如他所做的那样,您为每个处理器提供一个重载,然后在 Base 中调用虚拟调用,该调用会将其传递给处理器,从而调用正确的重载,然后可以将其调度到处理器层次结构中。 @DeadMG 请注意,他并没有说您 /need/ 它用于双重调度,他说这将 / 有用 / 用于双重调度(以及多次调度)。 @Pontus 啊哈!实际问题的答案!在我接受之前让我多读一点... @Matthew Lowe:当然。但是,运行时函数生成将有助于动态行为。人类对科学测试很有用。这并不意味着使用它们是一个好主意或正确的主意。 @DeadMG 同意。仅仅因为它可以完成并不意味着你应该这样做。【参考方案5】:

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

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

我发誓,我在一两天前回答了这个确切的问题。

【讨论】:

不,这不是不必要和多余的。它在需要多次分派的各种情况下都很有用。 (例如,假设您不能修改 DerivedA/B。或者从语义上讲,将 Do() 放在 Base/Derived 中是没有意义的。如果问题不存在,我将提供一个更具体的示例t 清除。) 如果类不是 Base,则行为需要改变的任何函数都进入虚拟层次结构。如果您的语义与此不同,那么是时候修复您的语义了。如果您无法更改 Derived,请向任何可以更改的人投诉,因为他们搞砸了您。 好吧,假设您有一个 Fruit 类,其派生类为 Apple 和 Orange。您正在尝试实现一个多态 Eat() 函数,该函数为苹果(清洗)或橙子(剥皮)做适当的事情。将 Eat() 放在 Fruit 类中是没有意义的,因为 Fruit 不应该知道如何吃自己。使用 dynamic_cast 或访问者模式进行双重调度可以解决这个问题,但我提到的 CAST_TO_MOST_DERIVED 也可以解决这个问题。 将 Eat 放在 Fruit 类中确实有意义,因为 Eat 取决于您拥有的 Fruit。【参考方案6】:

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

【讨论】:

【参考方案7】:

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

【讨论】:

实际上,我并不是在寻找双重调度,因为我已经意识到了。相反,我想知道为什么这个语言功能不可用。【参考方案8】:

这在 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);

【讨论】:

【参考方案9】:

为什么 C++ 没有它?也许创作者从来没有想过它。或者他们认为它不够合适或不够有用。或者,在实际尝试使用这种语言时可能存在问题。

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

假设存在此功能,以便编译器编写代码来检查指向的动态类型并调用适当的重载。现在还可以说代码的单独部分有class DerivedC : Base ...;。并说对应的Processor::Do重载加了。

考虑到所有这些,当程序尝试选择合适的重载时应该怎么做?在编译时无法捕捉到这种差异。它是否应该尝试爬上类层次结构以找到与基类匹配的函数?它应该抛出一个特殊的异常吗?它应该崩溃吗?有没有其他的可能?在不知道代码和类层次结构的意图的情况下,编译器实际上可以自行做出任何合理的选择吗?

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

【讨论】:

【参考方案10】:

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

【讨论】:

以上是关于为啥 C++ 不允许您请求指向最派生类的指针?的主要内容,如果未能解决你的问题,请参考以下文章

为啥指向基类的派生类指针可以调用派生类成员函数? [复制]

关于C++基类、派生类的引用和指针

在 C++ 中将指向基类的指针传递给派生类的成员函数

在 C++ 中将派生类对象分配和访问到基类“指向指针”对象

声明指向基类和派生类的指针

指向派生对象的基类指针的 C++ 排序容器