C++ 吗?派生对象上的虚函数调用要通过vtable吗?

发布于 2024-10-08 03:56:05 字数 379 浏览 5 评论 0原文

在下面的代码中,它通过指向派生对象的指针调用虚拟函数 foo。这个调用会通过 vtable 还是会直接调用 B::foo

如果它通过 vtable,那么让它直接调用 B::foo 的 C++ 惯用方法是什么?我知道在这种情况下我总是指向 B

Class A
{
    public:
        virtual void foo() {}
};

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


int main()
{
    B* b = new B();
    b->foo();
}

In the following code, it calls a virtual function foo via a pointer to a derived object. Will this call go through the vtable or will it call B::foo directly?

If it goes via a vtable, what would be a C++ idiomatic way of making it call B::foo directly? I know that in this case I am always pointing to a B.

Class A
{
    public:
        virtual void foo() {}
};

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


int main()
{
    B* b = new B();
    b->foo();
}

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

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

发布评论

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

评论(6

国产ˉ祖宗 2024-10-15 03:56:05

如果启用了优化,大多数编译器都足够聪明,可以消除这种情况下的间接调用。但这只是因为您刚刚创建了对象并且编译器知道动态类型;可能存在您知道动态类型而编译器不知道的情况。

Most compilers will be smart enough to eliminate the indirect call in that scenario, if you have optimization enabled. But only because you just created the object and the compiler knows the dynamic type; there may be situations when you know the dynamic type and the compiler doesn't.

人海汹涌 2024-10-15 03:56:05

像往常一样,这个问题的答案是“如果这对您很重要,请查看发出的代码”。这是 g++ 在未选择优化的情况下产生的结果:

18     b->foo();
0x401375 <main+49>:  mov    eax,DWORD PTR [esp+28]
0x401379 <main+53>:  mov    eax,DWORD PTR [eax]
0x40137b <main+55>:  mov    edx,DWORD PTR [eax]
0x40137d <main+57>:  mov    eax,DWORD PTR [esp+28]
0x401381 <main+61>:  mov    DWORD PTR [esp],eax
0x401384 <main+64>:  call   edx

它使用 vtable。由如下代码生成的直接调用:

B b;
b.foo();

如下所示:

0x401392 <main+78>:  lea    eax,[esp+24]
0x401396 <main+82>:  mov    DWORD PTR [esp],eax
0x401399 <main+85>:  call   0x40b2d4 <_ZN1B3fooEv>

As usual, the answer to this question is "if it is important to you, take a look at the emitted code". This is what g++ produces with no optimisations selected:

18     b->foo();
0x401375 <main+49>:  mov    eax,DWORD PTR [esp+28]
0x401379 <main+53>:  mov    eax,DWORD PTR [eax]
0x40137b <main+55>:  mov    edx,DWORD PTR [eax]
0x40137d <main+57>:  mov    eax,DWORD PTR [esp+28]
0x401381 <main+61>:  mov    DWORD PTR [esp],eax
0x401384 <main+64>:  call   edx

which is using the vtable. A direct call, produced by code like:

B b;
b.foo();

looks like this:

0x401392 <main+78>:  lea    eax,[esp+24]
0x401396 <main+82>:  mov    DWORD PTR [esp],eax
0x401399 <main+85>:  call   0x40b2d4 <_ZN1B3fooEv>
笨笨の傻瓜 2024-10-15 03:56:05

是的,它将使用 vtable(只有非虚拟方法绕过 vtable)。要直接在 b 上调用 B::foo(),请调用 b->B::foo()

Yes, it will use the vtable (only non-virtual methods bypass the vtable). To call B::foo() on b directly, call b->B::foo().

素手挽清风 2024-10-15 03:56:05

这是使用 -O3 从 g++ (4.5) 编译的代码。

_ZN1B3fooEv:
    rep
    ret

main:
    subq    $8, %rsp
    movl    $8, %edi
    call    _Znwm
    movq    $_ZTV1B+16, (%rax)
    movq    %rax, %rdi
    call    *_ZTV1B+16(%rip)
    xorl    %eax, %eax
    addq    $8, %rsp
    ret

_ZTV1B:
    .quad   0
    .quad   _ZTI1B
    .quad   _ZN1B3fooEv

它所做的唯一优化是它知道要使用哪个 vtable(在 b 对象上)。否则“call *_ZTV1B+16(%rip)”将是“movq (%rax), %rax; call *(%rax)”。
所以g++在优化虚函数调用方面实际上相当糟糕。

This is the compiled code from g++ (4.5) with -O3

_ZN1B3fooEv:
    rep
    ret

main:
    subq    $8, %rsp
    movl    $8, %edi
    call    _Znwm
    movq    $_ZTV1B+16, (%rax)
    movq    %rax, %rdi
    call    *_ZTV1B+16(%rip)
    xorl    %eax, %eax
    addq    $8, %rsp
    ret

_ZTV1B:
    .quad   0
    .quad   _ZTI1B
    .quad   _ZN1B3fooEv

The only optimization it did was that it knew which vtable to use (on the b object). Otherwise "call *_ZTV1B+16(%rip)" would have been "movq (%rax), %rax; call *(%rax)".
So g++ is actually quite bad at optimizing virtual function calls.

北渚 2024-10-15 03:56:05

编译器可以优化虚拟调度并直接调用虚拟函数或内联它(如果可以证明它是相同的行为)。在提供的示例中,编译器将轻松丢弃每一行代码,因此您将得到的是:

int main() {}

Compiler can optimize away virtual dispatch and call virtual function directly or inline it if it can prove it's the same behavior. In the provided example, compiler will easily throw away every line of code, so all you'll get is this:

int main() {}
清泪尽 2024-10-15 03:56:05

我对代码做了一些修改,以便自己尝试一下,对我来说,它看起来像是删除了 vtable,但我在 asm 方面还不够专业,无法判断。我确信一些评论员会纠正我的观点:)

struct A {
    virtual int foo() { return 1; }
};

struct B : public A {
    virtual int foo() { return 2; }
};

int useIt(A* a) {
    return a->foo();
}

int main()
{
    B* b = new B();
    return useIt(b);
}

然后我将这段代码转换为如下所示的程序集:

g++ -g -S -O0  -fverbose-asm virt.cpp 
as -alhnd virt.s > virt.base.asm
g++ -g -S -O6  -fverbose-asm virt.cpp 
as -alhnd virt.s > virt.opt.asm

在我看来,有趣的部分就像“opt”版本正在删除 vtable。看起来它正在创建 vtable,但没有使用它。

在 opt asm:

9:virt.cpp      **** int useIt(A* a) { 
89                    .loc 1 9 0 
90                    .cfi_startproc 
91                .LVL2: 
10:virt.cpp      ****     return a->foo(); 
92                    .loc 1 10 0 
93 0000 488B07        movq    (%rdi), %rax    # a_1(D)->_vptr.A, a_1(D)->_vptr.A 
94 0003 488B00        movq    (%rax), %rax    # *D.2259_2, *D.2259_2 
95 0006 FFE0          jmp *%rax   # *D.2259_2 
96                .LVL3: 
97                    .cfi_endproc 

和相同的 base.asm 版本中:

  9:virt.cpp      **** int useIt(A* a) { 
  88                    .loc 1 9 0 
  89                    .cfi_startproc 
  90 0000 55            pushq   %rbp    # 
  91                .LCFI6: 
  92                    .cfi_def_cfa_offset 16 
  93                    .cfi_offset 6, -16 
  94 0001 4889E5        movq    %rsp, %rbp  #, 
  95                .LCFI7: 
  96                    .cfi_def_cfa_register 6 
  97 0004 4883EC10      subq    $16, %rsp   #, 
  98 0008 48897DF8      movq    %rdi, -8(%rbp)  # a, a 
  10:virt.cpp      ****     return a->foo(); 
  99                    .loc 1 10 0 
 100 000c 488B45F8      movq    -8(%rbp), %rax  # a, tmp64 
 101 0010 488B00        movq    (%rax), %rax    # a_1(D)->_vptr.A, D.2263 
 102 0013 488B00        movq    (%rax), %rax    # *D.2263_2, D.2264 
 103 0016 488B55F8      movq    -8(%rbp), %rdx  # a, tmp65 
 104 001a 4889D7        movq    %rdx, %rdi  # tmp65, 
 105 001d FFD0          call    *%rax   # D.2264 
  11:virt.cpp      **** } 
 106                    .loc 1 11 0 
 107 001f C9            leave 
 108                .LCFI8: 
 109                    .cfi_def_cfa 7, 8 
 110 0020 C3            ret 
 111                    .cfi_endproc 

在第 93 行,我们在注释中看到: _vptr.A 我'我很确定这意味着它正在执行 vtable 查找,但是,在实际的主函数中,它似乎能够预测答案,甚至不调用该 useIt 代码:

 16:virt.cpp      ****     return useIt(b);
 17:virt.cpp      **** }
124                    .loc 1 17 0
125 0015 B8020000      movl    $2, %eax    #,

我认为这只是说,我们知道我们要返回2,让我们把它放在eax中。 (我重新运行该程序,要求它返回 200,并且该行已按我的预期更新)。


额外的一点

所以我把程序变得更复杂了一点:

struct A {
    int valA;
    A(int value) : valA(value) {}
    virtual int foo() { return valA; }
};

struct B : public A {
    int valB;
    B(int value) : valB(value), A(0) {}
    virtual int foo() { return valB; }
};

int useIt(A* a) {
    return a->foo();
}

int main()
{
    A* a = new A(100);
    B* b = new B(200);
    int valA = useIt(a);
    int valB = useIt(a);
    return valA + valB;
}

在这个版本中,useIt代码肯定使用了优化汇编中的vtable:

  13:virt.cpp      **** int useIt(A* a) {
  89                    .loc 1 13 0
  90                    .cfi_startproc
  91                .LVL2:
  14:virt.cpp      ****     return a->foo();
  92                    .loc 1 14 0
  93 0000 488B07        movq    (%rdi), %rax    # a_1(D)->_vptr.A, a_1(D)->_vptr.A
  94 0003 488B00        movq    (%rax), %rax    # *D.2274_2, *D.2274_2
  95 0006 FFE0          jmp *%rax   # *D.2274_2
  96                .LVL3:
  97                    .cfi_endproc

这一次,main函数内联了useIt<的副本/code>,但实际上执行了 vtable 查找。


c++11 和“final”关键字怎么样?

所以我将一行更改为:

virtual int foo() override final { return valB; }

并将编译器行更改为:

g++ -std=c++11 -g -S -O6  -fverbose-asm virt.cpp

认为告诉编译器它是最终重写,将允许它跳过也许是虚函数表。

事实证明它仍然使用vtable。


所以我的理论答案是:

  • 我不认为有任何明确的“不使用 vtable”优化。 (我在 g++ 手册页中搜索了 vtable 和 virt 等内容,但什么也没找到)。
  • 但是带有 -O6 的 g++ 可以对具有明显常量的简单程序进行大量优化,使其可以预测结果并完全跳过调用。
  • 然而,一旦事情变得复杂(读起来真实),它肯定会进行虚函数表查找,几乎每次调用虚函数时都是如此。

I changed the code up a bit to give it a go myself, and to me it looks like it's dropping the vtable, but I'm not expert enough in asm to tell. I'm sure some commentators will set me right though :)

struct A {
    virtual int foo() { return 1; }
};

struct B : public A {
    virtual int foo() { return 2; }
};

int useIt(A* a) {
    return a->foo();
}

int main()
{
    B* b = new B();
    return useIt(b);
}

I then converted this code to assembly like this:

g++ -g -S -O0  -fverbose-asm virt.cpp 
as -alhnd virt.s > virt.base.asm
g++ -g -S -O6  -fverbose-asm virt.cpp 
as -alhnd virt.s > virt.opt.asm

And the interesting bits look to me like the 'opt' version is dropping the vtable. It looks like it's creating the vtable but not using it..

In the opt asm:

9:virt.cpp      **** int useIt(A* a) { 
89                    .loc 1 9 0 
90                    .cfi_startproc 
91                .LVL2: 
10:virt.cpp      ****     return a->foo(); 
92                    .loc 1 10 0 
93 0000 488B07        movq    (%rdi), %rax    # a_1(D)->_vptr.A, a_1(D)->_vptr.A 
94 0003 488B00        movq    (%rax), %rax    # *D.2259_2, *D.2259_2 
95 0006 FFE0          jmp *%rax   # *D.2259_2 
96                .LVL3: 
97                    .cfi_endproc 

and the base.asm version of the same:

  9:virt.cpp      **** int useIt(A* a) { 
  88                    .loc 1 9 0 
  89                    .cfi_startproc 
  90 0000 55            pushq   %rbp    # 
  91                .LCFI6: 
  92                    .cfi_def_cfa_offset 16 
  93                    .cfi_offset 6, -16 
  94 0001 4889E5        movq    %rsp, %rbp  #, 
  95                .LCFI7: 
  96                    .cfi_def_cfa_register 6 
  97 0004 4883EC10      subq    $16, %rsp   #, 
  98 0008 48897DF8      movq    %rdi, -8(%rbp)  # a, a 
  10:virt.cpp      ****     return a->foo(); 
  99                    .loc 1 10 0 
 100 000c 488B45F8      movq    -8(%rbp), %rax  # a, tmp64 
 101 0010 488B00        movq    (%rax), %rax    # a_1(D)->_vptr.A, D.2263 
 102 0013 488B00        movq    (%rax), %rax    # *D.2263_2, D.2264 
 103 0016 488B55F8      movq    -8(%rbp), %rdx  # a, tmp65 
 104 001a 4889D7        movq    %rdx, %rdi  # tmp65, 
 105 001d FFD0          call    *%rax   # D.2264 
  11:virt.cpp      **** } 
 106                    .loc 1 11 0 
 107 001f C9            leave 
 108                .LCFI8: 
 109                    .cfi_def_cfa 7, 8 
 110 0020 C3            ret 
 111                    .cfi_endproc 

On line 93 we see in the comments: _vptr.A which I'm pretty sure means it's doing a vtable lookup, however, in the actual main function, it seems to be able to predict the answer and doesn't even call that useIt code:

 16:virt.cpp      ****     return useIt(b);
 17:virt.cpp      **** }
124                    .loc 1 17 0
125 0015 B8020000      movl    $2, %eax    #,

which I think is just saying, we know we're gonna return 2, lets just put it in eax. (I re ran the program asking it to return 200, and that line got updated as I would expect).


extra bit

So I complicated the program up a bit more:

struct A {
    int valA;
    A(int value) : valA(value) {}
    virtual int foo() { return valA; }
};

struct B : public A {
    int valB;
    B(int value) : valB(value), A(0) {}
    virtual int foo() { return valB; }
};

int useIt(A* a) {
    return a->foo();
}

int main()
{
    A* a = new A(100);
    B* b = new B(200);
    int valA = useIt(a);
    int valB = useIt(a);
    return valA + valB;
}

In this version, the useIt code definitely uses the vtable in the optimized assembly:

  13:virt.cpp      **** int useIt(A* a) {
  89                    .loc 1 13 0
  90                    .cfi_startproc
  91                .LVL2:
  14:virt.cpp      ****     return a->foo();
  92                    .loc 1 14 0
  93 0000 488B07        movq    (%rdi), %rax    # a_1(D)->_vptr.A, a_1(D)->_vptr.A
  94 0003 488B00        movq    (%rax), %rax    # *D.2274_2, *D.2274_2
  95 0006 FFE0          jmp *%rax   # *D.2274_2
  96                .LVL3:
  97                    .cfi_endproc

This time, the main function inlines a copy of useIt, but does actually do the vtable lookup.


What about c++11 and the 'final' keyword?

So I changed one line to:

virtual int foo() override final { return valB; }

and the compiler line to:

g++ -std=c++11 -g -S -O6  -fverbose-asm virt.cpp

Thinking that telling the compiler that it is a final override, would allow it to skip the vtable maybe.

Turns out it still uses the vtable.


So my theoretical answer would be:

  • I don't think there are any explicit, "don't use the vtable" optimizations. (I searched through the g++ manpage for vtable and virt and the like and found nothing).
  • But g++ with -O6, can do a lot of optimization on a simple program with obvious constants to the point where it can predict the result and skip the call altogether.
  • However, once things get complex (read real) it's definitely doing vtable lookups, pretty much everytime you call a virtual function.
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文