Это зависит от вашей целевой архитектуры и вашего компилятора, но вы можете написать небольшой тест и проверить сгенерированную сборку.
Я сделал один, чтобы сделать тест:
// test.h
#ifndef FOO_H
#define FOO_H
void bar();
class A {
public:
virtual ~A();
virtual void foo();
};
#endif
// main.cpp
#include "test.h"
void doFunctionPointerCall(void (*func)()) {
func();
}
void doVirtualCall(A *a) {
a->foo();
}
int main() {
doFunctionPointerCall(bar);
A a;
doVirtualCall(&a);
return 0;
}
Обратите внимание, что вам даже не нужно писать test.cpp, поскольку вам просто нужно проверить сборку для main.cpp.
Чтобы увидеть выходные данные сборки компилятора, с gcc используйте флаг -S:
gcc main.cpp -S -O3
Это создаст файл main.s, с выводом сборки.
Теперь мы можем видеть, что сгенерировал gcc для вызовов.
doFunctionPointerCall:
.globl _Z21doFunctionPointerCallPFvvE
.type _Z21doFunctionPointerCallPFvvE, @function
_Z21doFunctionPointerCallPFvvE:
.LFB0:
.cfi_startproc
jmp *%rdi
.cfi_endproc
.LFE0:
.size _Z21doFunctionPointerCallPFvvE, .-_Z21doFunctionPointerCallPFvvE
doVirtualCall:
.globl _Z13doVirtualCallP1A
.type _Z13doVirtualCallP1A, @function
_Z13doVirtualCallP1A:
.LFB1:
.cfi_startproc
movq (%rdi), %rax
movq 16(%rax), %rax
jmp *%rax
.cfi_endproc
.LFE1:
.size _Z13doVirtualCallP1A, .-_Z13doVirtualCallP1A
Обратите внимание, здесь я использую x86_64, сборка изменится для других архитектур.
Глядя на сборку, похоже, что она использует два дополнительных movq для виртуального вызова, возможно, это некоторое смещение в vtable. Обратите внимание, что в реальном коде необходимо сохранить некоторые регистры (будь то указатель на функцию или виртуальный вызов), но для виртуального вызова все равно потребуется два дополнительных указателя movq поверх функции.