覆盖虚拟成员函数时,为什么覆盖函数总是变为虚拟?

时间:2022-09-25 14:47:15

When I write like this:

我写这样的时候:

class A {
    public: virtual void foo() = 0;
}

class B {
    public: void foo() {}
}

...B::foo() becomes virtual as well. What is the rationale behind this? I would expect it to behave like the final keyword in Java.

... B :: foo()也变为虚拟。这背后的理由是什么?我希望它的行为类似于Java中的final关键字。

Add: I know that works like this and how a vtable works :) The question is, why C++ standard committee did not leave an opening to call B::foo() directly and avoid a vtable lookup.

补充:我知道这样的工作方式和vtable如何工作:)问题是,为什么C ++标准委员会没有留下直接调用B :: foo()并避免vtable查找的开头。

6 个解决方案

#1


9  

The standard does leave an opening to call B::foo directly and avoid a table lookup:

该标准确实留下了一个直接调用B :: foo并打开表查找的开头:

#include <iostream>

class A {
    public: virtual void foo() = 0;
};

class B : public A {
    public: void foo() {
        std::cout <<"B::foo\n";
    }
};

class C : public B {
    public: void foo() {
        std::cout <<"C::foo\n";
    }
};

int main() {
    C c;
    A *ap = &c;
    // virtual call to foo
    ap->foo();
    // virtual call to foo
    static_cast<B*>(ap)->foo();
    // non-virtual call to B::foo
    static_cast<B*>(ap)->B::foo();
}

Output:

C::foo
C::foo
B::foo

So you can get the behaviour you say you expect as follows:

因此,您可以获得您所期望的行为,如下所示:

class A {
    virtual void foo() = 0;
    // makes a virtual call to foo
    public: void bar() { foo(); }
};

class B : public A {
    void foo() {
        std::cout <<"B::foo\n";
    }
    // makes a non-virtual call to B::foo
    public: void bar() { B::foo(); }
};

Now callers should use bar instead of foo. If they have a C*, then they can cast it to A*, in which case bar will call C::foo, or they can cast it to B*, in which case bar will call B::foo. C can override bar again if it wants, or else not bother, in which case calling bar() on a C* calls B::foo() as you'd expect.

现在调用者应该使用bar而不是foo。如果他们有C *,那么他们可以将它转换为A *,在这种情况下bar会调用C :: foo,或者他们可以将它转换为B *,在这种情况下bar将调用B :: foo。如果它想要,C可以再次覆盖bar,否则不会打扰,在这种情况下,调用C *上的bar()会调用B :: foo(),如你所料。

I don't know when anyone would want this behaviour, though. The whole point of virtual functions is to call the same function for a given object, no matter what base or derived class pointer you're using. C++ therefore assumes that if calls to a particular member function through a base class are virtual, then calls through derived classes should also be virtual.

但我不知道何时会有人想要这种行为。虚函数的重点是为给定对象调用相同的函数,无论您使用的是什么基类或派生类指针。因此,C ++假设如果通过基类对特定成员函数的调用是虚拟的,那么通过派生类的调用也应该是虚拟的。

#2


6  

When you declare a virtual method, you're basically adding a new entry in the vtable. Overriding a virtual method changes the value of that entry; it doesn't remove it. This is basically true for languages like Java or C# too. The difference is that, with final keyword in Java, you can ask the compiler to arbitrarily enforce not being able to override it. C++ does not provide this language feature.

声明虚拟方法时,基本上是在vtable中添加新条目。覆盖虚方法会更改该条目的值;它不会删除它。对于Java或C#等语言来说基本上也是如此。不同之处在于,使用Java中的final关键字,您可以要求编译器任意强制执行无法覆盖它。 C ++不提供此语言功能。

#3


5  

Just because the class is forced to have a vtable, doesn't mean the compiler is forced to use it. If the type of the object is known statically, the compiler is free to bypass the vtable as an optimization. For example, B::foo will probably be called directly in this situation:

仅仅因为该类被强制使用vtable,并不意味着编译器*使用它。如果对象的类型是静态已知的,则编译器可以*地绕过vtable作为优化。例如,在这种情况下可能会直接调用B :: foo:

B b;
b.foo();

Unfortunately the only way I know to verify this is to look at the generated assembly code.

不幸的是,我知道验证这一点的唯一方法是查看生成的汇编代码。

#4


2  

Because technically it is virtual whatever you do — it has its place in the table. The rest would be a syntactical law enforcement and this is where C++ is different from java.

因为从技术上讲,无论你做什么,它都是虚拟的 - 它在表格中占有一席之地。其余的将是一个语法执法,这是C ++与java不同的地方。

#5


1  

A vtable is created for the base class when the first virtual function is defined. In your example foo() has an entry in the vtable. When the derived class inherits from the base class it inherits the vtable as well. The derived class must have an entry for foo() in its vtable so that the call will be redirected appropriately when the derived class is referenced polymorphically through a base class pointer.

在定义第一个虚函数时,为基类创建一个vtable。在您的示例中,foo()在vtable中有一个条目。当派生类继承自基类时,它也继承了vtable。派生类必须在其vtable中具有foo()的条目,以便在通过基类指针以多态方式引用派生类时,将适当地重定向调用。

#6


0  

It seems that at least Visual Studio is able to take advantage of the final keyword to skip vtable lookup, eg this code:

似乎至少Visual Studio能够利用final关键字跳过vtable查找,例如这段代码:

class A {
public:
    virtual void foo() = 0;
};
class B : public A {
public:
    void foo() final {}
};
B original;
B& b = original;
b.foo();
b.B::foo();

Produces the same code for b.foo() and for b.B::foo():

为b.foo()和b.B :: foo()生成相同的代码:

        b.foo();
000000013F233AA9  mov         rcx,qword ptr [b]
000000013F233AAE  call        B::foo (013F1B4F48h)
        b.B::foo();
000000013F233AB3  mov         rcx,qword ptr [b]
000000013F233AB8  call        B::foo (013F1B4F48h)

Whereas without the final it uses the lookup table:

而没有最终它使用查找表:

        b.foo();
000000013F893AA9  mov         rax,qword ptr [b]
000000013F893AAE  mov         rax,qword ptr [rax]
000000013F893AB1  mov         rcx,qword ptr [b]
000000013F893AB6  call        qword ptr [rax]
        b.B::foo();
000000013F893AB8  mov         rcx,qword ptr [b]
000000013F893ABD  call        B::foo (013F814F48h)

I don't know whether other compilers do the same, though.

不过,我不知道其他编译器是否也这样做。

#1


9  

The standard does leave an opening to call B::foo directly and avoid a table lookup:

该标准确实留下了一个直接调用B :: foo并打开表查找的开头:

#include <iostream>

class A {
    public: virtual void foo() = 0;
};

class B : public A {
    public: void foo() {
        std::cout <<"B::foo\n";
    }
};

class C : public B {
    public: void foo() {
        std::cout <<"C::foo\n";
    }
};

int main() {
    C c;
    A *ap = &c;
    // virtual call to foo
    ap->foo();
    // virtual call to foo
    static_cast<B*>(ap)->foo();
    // non-virtual call to B::foo
    static_cast<B*>(ap)->B::foo();
}

Output:

C::foo
C::foo
B::foo

So you can get the behaviour you say you expect as follows:

因此,您可以获得您所期望的行为,如下所示:

class A {
    virtual void foo() = 0;
    // makes a virtual call to foo
    public: void bar() { foo(); }
};

class B : public A {
    void foo() {
        std::cout <<"B::foo\n";
    }
    // makes a non-virtual call to B::foo
    public: void bar() { B::foo(); }
};

Now callers should use bar instead of foo. If they have a C*, then they can cast it to A*, in which case bar will call C::foo, or they can cast it to B*, in which case bar will call B::foo. C can override bar again if it wants, or else not bother, in which case calling bar() on a C* calls B::foo() as you'd expect.

现在调用者应该使用bar而不是foo。如果他们有C *,那么他们可以将它转换为A *,在这种情况下bar会调用C :: foo,或者他们可以将它转换为B *,在这种情况下bar将调用B :: foo。如果它想要,C可以再次覆盖bar,否则不会打扰,在这种情况下,调用C *上的bar()会调用B :: foo(),如你所料。

I don't know when anyone would want this behaviour, though. The whole point of virtual functions is to call the same function for a given object, no matter what base or derived class pointer you're using. C++ therefore assumes that if calls to a particular member function through a base class are virtual, then calls through derived classes should also be virtual.

但我不知道何时会有人想要这种行为。虚函数的重点是为给定对象调用相同的函数,无论您使用的是什么基类或派生类指针。因此,C ++假设如果通过基类对特定成员函数的调用是虚拟的,那么通过派生类的调用也应该是虚拟的。

#2


6  

When you declare a virtual method, you're basically adding a new entry in the vtable. Overriding a virtual method changes the value of that entry; it doesn't remove it. This is basically true for languages like Java or C# too. The difference is that, with final keyword in Java, you can ask the compiler to arbitrarily enforce not being able to override it. C++ does not provide this language feature.

声明虚拟方法时,基本上是在vtable中添加新条目。覆盖虚方法会更改该条目的值;它不会删除它。对于Java或C#等语言来说基本上也是如此。不同之处在于,使用Java中的final关键字,您可以要求编译器任意强制执行无法覆盖它。 C ++不提供此语言功能。

#3


5  

Just because the class is forced to have a vtable, doesn't mean the compiler is forced to use it. If the type of the object is known statically, the compiler is free to bypass the vtable as an optimization. For example, B::foo will probably be called directly in this situation:

仅仅因为该类被强制使用vtable,并不意味着编译器*使用它。如果对象的类型是静态已知的,则编译器可以*地绕过vtable作为优化。例如,在这种情况下可能会直接调用B :: foo:

B b;
b.foo();

Unfortunately the only way I know to verify this is to look at the generated assembly code.

不幸的是,我知道验证这一点的唯一方法是查看生成的汇编代码。

#4


2  

Because technically it is virtual whatever you do — it has its place in the table. The rest would be a syntactical law enforcement and this is where C++ is different from java.

因为从技术上讲,无论你做什么,它都是虚拟的 - 它在表格中占有一席之地。其余的将是一个语法执法,这是C ++与java不同的地方。

#5


1  

A vtable is created for the base class when the first virtual function is defined. In your example foo() has an entry in the vtable. When the derived class inherits from the base class it inherits the vtable as well. The derived class must have an entry for foo() in its vtable so that the call will be redirected appropriately when the derived class is referenced polymorphically through a base class pointer.

在定义第一个虚函数时,为基类创建一个vtable。在您的示例中,foo()在vtable中有一个条目。当派生类继承自基类时,它也继承了vtable。派生类必须在其vtable中具有foo()的条目,以便在通过基类指针以多态方式引用派生类时,将适当地重定向调用。

#6


0  

It seems that at least Visual Studio is able to take advantage of the final keyword to skip vtable lookup, eg this code:

似乎至少Visual Studio能够利用final关键字跳过vtable查找,例如这段代码:

class A {
public:
    virtual void foo() = 0;
};
class B : public A {
public:
    void foo() final {}
};
B original;
B& b = original;
b.foo();
b.B::foo();

Produces the same code for b.foo() and for b.B::foo():

为b.foo()和b.B :: foo()生成相同的代码:

        b.foo();
000000013F233AA9  mov         rcx,qword ptr [b]
000000013F233AAE  call        B::foo (013F1B4F48h)
        b.B::foo();
000000013F233AB3  mov         rcx,qword ptr [b]
000000013F233AB8  call        B::foo (013F1B4F48h)

Whereas without the final it uses the lookup table:

而没有最终它使用查找表:

        b.foo();
000000013F893AA9  mov         rax,qword ptr [b]
000000013F893AAE  mov         rax,qword ptr [rax]
000000013F893AB1  mov         rcx,qword ptr [b]
000000013F893AB6  call        qword ptr [rax]
        b.B::foo();
000000013F893AB8  mov         rcx,qword ptr [b]
000000013F893ABD  call        B::foo (013F814F48h)

I don't know whether other compilers do the same, though.

不过,我不知道其他编译器是否也这样做。