在虚方法中调用虚方法时,是否应该发生虚分派

Should virtual dispatch happen when a virtual method is called within a virtual method using object?

本文关键字:方法 分派 调用 是否      更新时间:2023-10-16
struct B
{
  virtual void bar () {}
  virtual void foo () { bar(); }
};
struct D : B
{
  virtual void bar () {}
  virtual void foo () {}
};

现在我们用B的对象称foo()

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

:应该通过virtual调度来解析bar(),还是使用对象的静态类型(即B)来解析。

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

原始回答:

它将被虚拟地调度。当你考虑到在一个类方法中,一个方法调用是像this->bar();这样的东西时,这一点就更明显了,这使得很明显,一个指针被用来调用方法,允许使用动态对象类型。

然而,在你的例子中,因为你创建了一个B,它当然会调用B的方法版本。

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

EDIT2为您更新:这完全不对。B::foo()中的调用通常不能静态地绑定(除非由于内联编译器知道对象的静态类型)。仅仅因为它知道它是在B*上被调用的,并不能说明所讨论对象的实际类型——它可能是D*,并且需要虚拟调度。

必须是虚呼叫。您正在编译的代码无法知道是否存在一个派生程度更高的类,该类实际上已经覆盖了另一个函数。

注意,这假定您正在分别编译它们。如果编译器内联了对foo()的调用(因为它的静态类型是已知的),它也会内联对bar()的调用。

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

底线是,从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()的调用。

完全取决于实现。名义上它是一个虚调用,但您不能假设所发出的代码实际上会通过虚函数表或类似的方法执行间接调用。

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

这不是一个任意的B*,这是一个动态类型的B对象。虚函数或非虚函数调用的结果是完全相同的,所以编译器可以随心所欲("as-if"规则),而一个符合规则的程序无法分辨出两者的区别。

特别是在这种情况下,如果对foo的调用是内联的,那么我认为优化器有充分的机会在它内部对bar的调用进行非虚拟化,因为它确切地知道obj的虚表(或等效)中有什么。如果调用不是内联的,那么它将使用foo()的"香草"代码,这当然需要做一些间接操作,因为它与在任意B*上进行调用时使用的代码相同。

在本例中:

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&在这个例子中是完全不必要的。