在使用对象的虚拟方法中调用虚方法时是否应该发生虚拟调度?

时间:2011-08-18 14:58:59

标签: c++ virtual-method

struct B
{
  virtual void bar () {}
  virtual void foo () { bar(); }
};

struct D : B
{
  virtual void bar () {}
  virtual void foo () {}
};

现在我们使用foo()对象

来调用B
B obj;
obj.foo();  // calls B::bar()

问题bar()是否应通过virtual发送解决,或者使用对象的静态类型(即B)来解析。

有趣的更新

如果允许编译器解析对象的类型,则可能存在未写入的规则:

无论使用对象或指针/引用调用foo(),都应静态解析任何virtual B::foo()内的所有调用。因为,一旦你进入B::foo(),就确定this的类型为B*,而不是D*

5 个答案:

答案 0 :(得分:3)

编辑:我想我误解了你的问题。我很确定这取决于编译器的优化器有多聪明。一个天真的实现当然仍然会通过虚拟查找。确定特定实现的唯一方法是编译代码并查看反汇编以查看它是否足够智能以进行直接调用。

原始答案:

它将被虚拟派遣。当您考虑在类方法中,方法调用适用于类似this->bar();之类的东西时,这一点就更明显了,很明显,指针用于调用方法,允许使用动态对象类型。

但是,在您创建B之后的示例中,它当然会调用B的方法版本。

请注意(如注释中所示)即使使用隐式this->,虚拟调度也不会在构造函数内部发生。

EDIT2供您更新: 这根本不对。 B::foo()内的调用通常不能静态绑定(除非由于内联编译器知道对象的静态类型)。只是因为它知道它在B*上被调用时没有提到有关对象的真实类型 - 它可能是D*并且需要虚拟调度。

答案 1 :(得分:2)

必须是虚拟通话。您正在编译的代码无法知道是否实际上没有更多派生类覆盖了其他函数。

请注意,这假设您要单独编译这些内容。如果编译器内联对foo()的调用(由于其静态类型已知),它还会内联对bar()的调用。

答案 2 :(得分:2)

答案:从语言的角度来看,bar()内部B::foo()的呼叫是通过虚拟调度解决的。

最重要的是,从C ++语言的角度来看,当您使用非限定名称调用虚拟方法时,会发生虚拟调度始终。使用方法的限定名称时,不会发生虚拟分派。这意味着在C ++中禁止虚拟分派的唯一方法是使用此语法

some_object_ptr->SomeClass::some_method();
some_object.SomeClass::some_method();

在这种情况下,如果忽略左侧的对象并直接调用特定方法,则为动态类型。

在所有其他情况下,就语言而言,虚拟调度确实会发生。即根据对象的动态类型解决调用。换句话说,从形式的角度来看,每次通过直接对象调用虚方法时,如

B obj;
obj.foo();

通过“虚拟调度”机制调用该方法,无论上下文如何(“在虚拟方法中”或不在 - 无关紧要)。

这就是C ++语言的方式。其他一切都只是编译器的优化。您可能知道,当通过直接对象执行调用时,大多数(如果不是全部)编译器将生成对虚方法的非虚拟调用。当然,这是一个明显的优化,因为编译器知道对象的静态类型与其动态类型相同。同样,它不依赖于上下文(“在虚拟方法中”或不依赖 - 无关紧要)。

在虚拟方法中,可以在不指定左侧的对象的情况下进行调用(如您的示例中所示),这实际上意味着左侧隐式存在所有调用this->。同样的规则也适用于这种情况。如果您只是致电bar(),则代表this->bar()并且虚拟调度呼叫。如果您拨打B::bar(),则代表this->B::bar(),并且非虚拟地发送呼叫。其他所有内容都只取决于编译器的优化功能。

你想说的是“因为,一旦你进入B::foo(),它确定这是B*类型而不是D*”对我来说完全不清楚。这句话忽略了这一点。虚拟调度取决于对象动态类型。注意:它取决于*this的类型,而不是this的类型。完全没有this的类型。重要的是*this的动态类型。当您在B::foo内时,动态类型*this仍然完全可能是D或其他内容。出于这个原因,必须动态解决对bar()的调用。

答案 3 :(得分:1)

完全取决于实施。名义上它是一个虚拟调用,但你无权假设发出的代码实际上会通过vtable或类似的方式执行间接调用。

如果在某个foo()上调用B*,那么为foo()发出的代码当然需要对bar()进行虚拟调用,因为该引用可能属于到派生类。

这不是任意B*,这是动态类型B的对象。虚拟或非虚拟调用的结果完全相同,因此编译器可以执行它喜欢的操作(“as-if”规则),并且符合标准的程序无法区分。

特别是在这种情况下,如果对foo的调用是内联的,那么我认为优化器有可能将对bar内部的调用去虚拟化,因为它知道究竟是obj的vtable(或等价物)中的内容。如果没有内联调用,那么它将使用foo()的“vanilla”代码,这当然需要做某种间接,因为它是在对任意调用时使用的相同代码B*

答案 4 :(得分:1)

在这种情况下:

B obj;
obj.foo();  // calls B::bar()

编译器可以优化掉虚拟调度,因为它知道实际对象的类型是B

但是,在B::foo()内部,对bar()的调用通常需要使用虚拟调度(尽管编译器可能能够内联调用以及该特定调用实例可能会再次优化虚拟调度)。具体来说,您提出的这个陈述:

  

无论使用对象或指针/引用调用foo(),都应静态解析任何虚拟B::foo()内的所有调用。因为,一旦你进入B :: foo(),它确定这是B*类型而不是D*

不是真的。

考虑:

struct D2 : B
{
    // D2 does not override bar()
    virtual void foo () {
        cout << "hello from D2::bar()" << endl;
    }
};

现在,如果你有以下某个地方:

D2 test;

B& bref = test;

bref.foo();

foo()的调用最终会在B::foo(),但当B::foo()调用bar()时,需要调度到D2::bar()

实际上,现在我输入了这个,B&对于这个例子来说完全没必要。