Можно ли реализовать обход vtable для виртуальных функций?

Вместо использования виртуальных функций, в которых выполняется поиск указателя vtable в объекте, который затем приводит вас к vtable, содержащему указатель на функцию, нельзя ли просто содержать элемент данных в объекте, который указывает прямо к функции?


person user997112    schedule 24.11.2014    source источник
comment
Если компилятор знает статический тип объекта, он может оптимизировать вызов, чтобы он был прямым вызовом. Я думаю, что эта оптимизация распространена, но я не уверен на 100%. Если компилятор не может этого сделать, то наличие вашего собственного указателя в объекте (и любого кода, который вам понадобится, чтобы воспользоваться им), вероятно, будет намного менее оптимизированным, чем компилятор, просто использующий указатель в vtable.   -  person Michael Burr    schedule 24.11.2014
comment
Каков будет размер ваших объектов, если вы добавите все указатели в каждый из них?   -  person quantdev    schedule 24.11.2014
comment
это означает, что у вас есть для каждого виртуального члена элемент данных в каждом экземпляре. Это немного быстрее, но намного больше по размеру. Вы сохраняете только одно косвенное обращение для вызова, но получаете огромное количество потерянного пространства данных.   -  person Klaus    schedule 24.11.2014
comment
Хорошо, скажем, у моих объектов есть только один метод виртуального интерфейса. Это решение будет лучше? На самом деле вам даже не нужно хранить 8-байтовый указатель, вы можете просто сохранить смещение члена данных.   -  person user997112    schedule 24.11.2014
comment
@ user997112: нет, это должен быть указатель на функцию, которая хранится в коде программы, функции не находятся в пространстве объекта. Для этого потребуется полный 8-байтовый указатель. Скорее всего, если у вас есть виртуальная функция, вам также нужен виртуальный деструктор, поэтому вам понадобятся как минимум эти два 8-байтовых указателя.   -  person Mooing Duck    schedule 25.11.2014
comment
Связанный: stackoverflow.com/questions/5417829/   -  person Mooing Duck    schedule 25.11.2014


Ответы (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 по крайней мере так же эффективен, как и вызов через указатель функции. Компилятор берет на себя всю инициализацию и самые сложные ситуации наследования. Лучше используйте мощные виртуальные функции вместо того, чтобы пытаться вручную превзойти свой компилятор.

person Christophe    schedule 24.11.2014
comment
Без включения оптимизации этот тест особо ничего полезного не демонстрирует. - person Mark Ransom; 25.11.2014
comment
@MarkRansom, вероятно, лучше всего было бы провести тест для конкретного случая OP, потому что вы не можете обобщать оптимизации, выполненные на упрощенном демонстрационном коде. - person Christophe; 25.11.2014