Используются ли Vtables только с указателем на базовый класс
Я знаю, что здесь задают много вопросов о vtables, но я все еще немного сбит с толку.
Используются ли vtables только тогда, когда у нас есть указатель на базовый класс, чтобы определить, какую виртуальную функцию производных классов вызывать?
В моем примере ниже, в случае 1, используются ли vtables здесь во время выполнения, даже если объект Tiger не был динамически создан в хранилище heap / free?
В случае 2 используются vtables, даже если компилятор во время компиляции знает, что мы указываем на объект Tiger.
А как насчет случая 3?
Заранее спасибо.
#include <iostream>
using namespace std;
class Animal // base class
{
public:
virtual void makeNoise() {cout<<" "<<endl;}
};
class Tiger: public Animal
{
public:
void makeNoise() {cout<<"Tiger Noise"<<endl;}
};
class Elephant: public Animal
{
public:
void makeNoise() {cout<<"Elephant Noise"<<endl;}
};
int main()
{
//case 1
Tiger t1;
Animal* aptr = &t1;
aptr->makeNoise(); // vtables used?
//case 2
Tiger t2;
Tiger* tptr = &t2; //vtables used ?
tptr->makeNoise();
//case 3
Elephant e1; //vtables used ?
e1.makeNoise();
}
3 ответа
Использует ли конкретный компилятор таблицу виртуальных функций или совершенно другой механизм для реализации динамической диспетчеризации виртуальных функций, зависит от внутренней реализации этого компилятора. Если вы хотите получить ответ о поведении конкретного компилятора, обратитесь к документации и / или исходному коду этого компилятора.
Сам язык C++ определяет, как должен работать вызов виртуальной функции, и оставляет это на усмотрение компилятора.
Стандарт требует, чтобы вызов виртуальной функции отправлялся окончательному переопределителю на основе динамического типа объекта, для которого вызывается функция. В вашем коде динамический тип t1
а также t2
является Tiger
и динамический тип e1
является Elephant
,
Да, большинство (если не все) компиляторы используют таблицу виртуальных функций для реализации вызовов виртуальных функций. Да, любой достойный компилятор должен максимизировать свои попытки разрешить динамическую диспетчеризацию во время компиляции, если он может это сделать, и заменить использование виртуальных таблиц прямым вызовом, когда это возможно (это проблема качества реализации для компилятора).
Какие из вызовов в вашем примере будут статически распределены, зависит от того, насколько "агрессивным" (или "умным", если вы предпочитаете) оптимизатор вашего компилятора.
Я бы сказал, что каждый здравомыслящий компилятор должен статически отправлять вызов через e1
даже с отключенными оптимизациями. Было бы совершенно ненужной пессимизацией вызывать там механизм динамической отправки.
Что касается звонков через aptr
а также tptr
, это зависит от того, способен ли статический анализатор оптимизатора вашего компилятора устранить aptr
а также tptr
, заменяя их использованием фактического объекта, на который они указывают (так как эта информация доступна во время компиляции). Приличный оптимизатор должен быть способен на это и отправлять все 3 вызова статически.
Чтобы убедиться, как ваш компилятор обрабатывает вызов, проверьте сгенерированную сборку.
Как говорится в других комментариях, использование vtables обрабатывается компилятором, который может попытаться оптимизировать свой доступ, если получаемый результат является ожидаемым.
Однако мы можем думать о vtables как о таблицах, которые содержат адреса виртуальных методов. Каждый вызов метода, который был объявлен "виртуальным" в родительском классе, должен проверять виртуальную таблицу во время выполнения, чтобы узнать конкретный адрес, куда следует перейти.
Такое поведение ожидают программисты, несмотря на то, что конкретный механизм может быть более сложным, и даже может вообще не полагаться на запросы к vtables, если компилятор может определить адрес во время компиляции.
Таким образом, во всех этих случаях компилятор может быть достаточно умен, чтобы установить адрес во время компиляции. Но вы должны просто полагаться на то, что в "наихудшем случае" доступ к vtable будет осуществляться в каждом случае, так как вы вызываете виртуальные методы (это ожидаемое поведение), и пусть компилятор выполняет оптимизацию, которую он считает это должно сделать.
Так же, как пояснение того, что вы говорите в случае 1, vtable access не имеет никакого отношения к тому, был ли объект размещен в куче или в стеке. Это совершенно разные понятия.
Мне было интересно посмотреть, как они составлены. Итак, вот что я увидел:
Clang 13 и GCC 11.2 дают одинаковые результаты. Сборка, о которой я упоминал ниже, взята из Clang:
Без оптимизации:
case 1
aptr->makeNoise();
assembly: call qword ptr [rax]
Call to pointer, therefore, vtable is used
case 2
tptr->makeNoise();
assembly: call qword ptr [rax]
Call to pointer, therefore, vtable is used
//case 3
e1.makeNoise();
assembly: call Elephant::makeNoise()
Direct call
лязг с
case 1
aptr->makeNoise();
assembly: call Tiger::makeNoise()
direct call
case 2
tptr->makeNoise();
assembly: call Tiger::makeNoise()
direct call
case 3
e1.makeNoise();
assembly: call Elephant::makeNoise()
Direct call
Дальнейшие оптимизации сделают
-O1
более оптимизирован.