Я немного изменил код, чтобы испытать его сам, и мне кажется, что он отбрасывает 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, но не использует его.
В опции:
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, однако в реальной основной функции он, похоже, способен предсказать ответ и даже не вызовите этот код использования:
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
На этот раз основная функция вставляет копию useIt
, но на самом деле выполняет поиск в vtable.
А как насчет c ++ 11 и ключевого слова 'final'?
Итак, я изменил одну строку на:
virtual int foo() override final { return valB; }
и строка компилятора:
g++ -std=c++11 -g -S -O6 -fverbose-asm virt.cpp
Думая, что, сообщив компилятору, что это окончательное переопределение, он может пропустить vtable, возможно.
Оказывается, он все еще использует vtable.
Так что мой теоретический ответ будет:
- Я не думаю, что есть какие-то явные оптимизации "не используйте vtable". (Я искал на странице руководства g ++ vtable, virt и т.п. и ничего не нашел).
- Но g ++ с -O6 может провести большую оптимизацию простой программы с очевидными константами до такой степени, что она может предсказать результат и вообще пропустить вызов.
- Однако, когда все становится сложным (читай реальным), он определенно выполняет поиск в vtable, почти каждый раз, когда вы вызываете виртуальную функцию.