Функция указателя на элемент выполняет виртуальную отправку?

#c #function-pointers #virtual-functions #member-pointers

Вопрос:

Я выполнил следующий код.

 #include <iostream>

class Base
{
public:
    virtual void func()
    {
        std::cout<<"Base func called"<<std::endl;
    }
};

class Derived: public Base
{
public:
    virtual void func()  override
    {
        std::cout<<"Derived func called"<<std::endl;
    }
};

int main()
{
    void (Base::*func_ptr)()=amp;Base::func; //Yes, the syntax is very beautiful.
    Base* bptr=new Derived();
    (bptr->*func_ptr)();
}
 

Мой ожидаемый результат был Base func called таким. Однако вместо этого результат был

 Derived func called
 

Что меня удивило, потому что я думаю , что func_ptr это должно позволять видеть только Base участников(потому что я думал, что func_ptr это не доступ к функции-члену через _vptr , но адрес самой функции.

Я хотел бы знать, как в этом случае происходит виртуальная отправка(как происходит доступ к виртуальной таблице) и где это поведение определено в стандарте C (я ничего не смог найти)?

Комментарии:

1. возможно, вы захотите проверить codeproject.com/Articles/7150/…

2. Я считаю, что это реализовано как «смещение», поэтому он будет отключаться от любого типа, на который он указывает, с this указателя плюс смещение. Поскольку он указывает на фактический производный объект, это и есть выполняемая функция.

3. Если вы подумаете об этом, когда вы пишете bptr->func , компилятор func Base::func тоже решает, только тогда во время выполнения он решается на правильный вызов.

4. В стандарте нет упоминания о виртуальных таблицах. Они являются деталями реализации конкретных компиляторов. Виртуальные таблицы — это не единственный способ реализации виртуальных методов, просто самый распространенный способ.

5. Указатель на функцию-член-это не то же самое, что указатель. У него достаточно данных для обработки виртуальной диспетчеризации, когда функция-член, на которую он указывает, является виртуальной функцией.

Ответ №1:

Обратитесь к [expr.вызов], в частности, здесь

[Если выбранная функция виртуальна], вызывается ее окончательное переопределение в динамическом типе выражения объекта; такой вызов называется вызовом виртуальной функции

Независимо от того, вызываете ли вы функцию с помощью указателя или с помощью доступа к члену класса, это одно и то же (ссылка); фактическая функция, вызываемая для виртуальной функции, в конечном счете зависит от фактического типа объекта, для которого она вызывается.

Несколько (ненормативных) примечаний в стандарте в разделе [class.virtual] говорят почти то же самое:

[Примечание 3: Интерпретация вызова виртуальной функции зависит от типа объекта, для которого она вызывается (динамический тип)

[Примечание 4: […] вызов виртуальной функции зависит от определенного объекта для определения, какую функцию вызывать.


Если вы хотите знать, как происходит отправка виртуальной функции, вам нужно будет найти конкретную реализацию, потому что » как » не стандартизировано.

(Мне очень понравилась статья «Базовый взгляд на виртуальную таблицу«, в которой показан один из возможных способов ее реализации с помощью C)

Комментарии:

1. Теперь, после редактирования, для меня это имеет больше смысла, спасибо.

2. Можно ли получить указатель функции, который не вызовет виртуальную отправку?

3. @MarekR возьмите указатель для типа срезанного объекта, если вы собираетесь привести его к базовому объекту, и переинтерпретируйте приведение указателя. Это УБ и на твоей совести.

4. @Swift-FridayPie если это UB, то это не решение. Также, пожалуйста, приведите пример, так как ваше описание для меня слишком расплывчатое.

5. @MarekR: Невозможно получить указатель на функцию- член , но, безусловно, можно получить указатель на функцию , если вы напишете другую функцию для переноса вызова по ссылке, которая явно вызовет функцию-член базового класса. Это может работать даже с не захватывающей лямбдой, например: godbolt.org/z/74WvGP698

Ответ №2:

  1. Ваше понимание неверно.
  2. Это деталь реализации. Разные компиляторы могут реализовывать его по-разному. Если вы сомневаетесь, посмотрите на сборку. На самом деле это довольно умно в данном конкретном компиляторе. Если значение, хранящееся в указателе на элемент, четное, это прямой указатель на функцию (все функции расположены по четным адресам). Если он нечетный, это смещение в таблице v плюс один. Все смещения vtable также являются четными, поэтому, чтобы добраться до фактического указателя, он уменьшается на 1.

Комментарии:

1. «Ваше понимание неверно» — Вот почему я здесь.