Вместо использования виртуальных функций, в которых выполняется поиск указателя vtable в объекте, который затем приводит вас к vtable, содержащему указатель на функцию, нельзя ли просто содержать элемент данных в объекте, который указывает прямо к функции?
Можно ли реализовать обход vtable для виртуальных функций?
Ответы (1)
Насколько я понимаю ваш вопрос, вы ищете способ реализовать полиморфизм с помощью указателя на функцию.
Что ж, это возможно, но чрезвычайно громоздко, подвержено ошибкам, но будет трудно превзойти сгенерированный вашим компилятором вызов виртуальных функций.
Как это сделать?
Идея состоит в том, чтобы использовать указатель функции. Чтобы реализовать полиморфы, он должен быть в базовом классе.
class B { // Base class
protected:
void (B::*fptest)(); // Function pointer to member function
public:
void mtest() // Class specific mebmer function
{ cout << "Base test\n"; }
void itest() // Polymorphic function
{ (this->*fptest)(); } // implemented by calling the poitner to member function
B() : fptest(&B::mtest) { } // Ctor must initialize the function pointer
virtual ~B() {}
};
class D : public B { // Derived class
public:
void mtest() // Class specific mebmer function
{ cout << "Derived test\n"; }
D() // Ctor
{ fptest = reinterpret_cast<void(B::*)()>(&D::mtest); } // not sure it's this safe in case of multiple inheritance !!
};
Код для проверки этой конструкции:
B b;
D d;
B *pb = &b, *pd = &d;
pb->itest();
pd->itest();
Это безопасно?
На это есть серьезные ограничения. Например:
- Вы должны убедиться, что каждый производный класс правильно инициализирует указатель на функцию.
- В случае множественного наследования указатель функции, приведенный к члену базового класса, может работать не так, как ожидалось.
- Перегрузка указателей невозможна. Поэтому вам понадобится другой указатель для каждой возможной подписи. Это может быть довольно странно.
Он более эффективен, чем поиск в vtable?
Нет: посмотрите на ассемблер, выполняемый для каждого полиморфного вызова itest()
:
; 41 : pd->itest(); // cod for the call for a derived object
mov ecx, DWORD PTR _pd$[ebp] ; load the oject address
call ?itest@B@@QAEXXZ ; call B::itest
; 16 : void itest() { (this->*fptest)(); }
push ebp
mov ebp, esp
sub esp, 68 ; 00000044H
push ebx
push esi
push edi
mov DWORD PTR _this$[ebp], ecx ; use address of object as parameter
mov eax, DWORD PTR _this$[ebp] ; load the function pointer
mov ecx, DWORD PTR _this$[ebp] ; " "
mov edx, DWORD PTR [eax+4] ; call the function pointer
call edx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
Конечно, оптимизатор может встроить код, удалив часть push и pop, но общий принцип заключается в том, что будет сгенерирован код для косвенного обращения.
Разве поиск в vtable недостаточно эффективен?
Ну, поиск в vtable в основном загружает указатель функции из фиксированного смещения, рассчитанного компилятором. Ассемблерный код для вызова виртуальной функции test() будет выглядеть так:
39 : pd->test();
mov eax, DWORD PTR _pd$[ebp]
mov edx, DWORD PTR [eax]
mov ecx, DWORD PTR _pd$[ebp]
mov eax, DWORD PTR [edx]
call eax
Заключение
Поиск в vtable по крайней мере так же эффективен, как и вызов через указатель функции. Компилятор берет на себя всю инициализацию и самые сложные ситуации наследования. Лучше используйте мощные виртуальные функции вместо того, чтобы пытаться вручную превзойти свой компилятор.