当重写虚拟成员函数时,为什么重写函数总是变成虚拟的?

发布于 2024-08-15 13:22:54 字数 301 浏览 3 评论 0原文

当我这样写时:

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

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

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

添加:我知道它是这样工作的以及 vtable 是如何工作的:) 问题是,为什么 C++ 标准委员会没有留下直接调用 B::foo() 并避免 vtable 查找的空缺。

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.

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

殤城〤 2024-08-22 13:22:54

该标准确实留下了直接调用 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();
}

输出:

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

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

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(); }
};

现在调用者应该使用 bar 而不是 foo。如果他们有 C*,那么他们可以将其转换为 A*,在这种情况下 bar 将调用 C::foo,或者他们可以将其转换为 B*,在这种情况下,bar 将调用B::foo。如果需要的话,C 可以再次覆盖 bar,否则不必打扰,在这种情况下,在 C* 上调用 bar() 会像您一样调用 B::foo()预计。

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

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

#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.

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.

陌伤ぢ 2024-08-22 13:22:54

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

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.

情深缘浅 2024-08-22 13:22:54

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

B b;
b.foo();

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

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:

B b;
b.foo();

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

爱要勇敢去追 2024-08-22 13:22:54

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

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.

汐鸠 2024-08-22 13:22:54

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

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.

治碍 2024-08-22 13:22:54

看来至少 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();

b.foo() 生成相同的代码并且对于 bB::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)

如果没有 final,它会使用查找表:

        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)

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

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

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();
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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文