Бывают ли случаи, когда класс объявляет виртуальные методы и компилятору не нужно использовать vptr?

Мне было интересно, существует ли возможная оптимизация, при которой компилятору не нужно назначать vptr созданному объекту, даже если тип объекта является классом с виртуальными методами.

Например, рассмотрим:

#include <iostream>
struct FooBase
{
  virtual void bar()=0;
};

struct FooDerived : public FooBase
{
  virtual void bar() { std::cout << "FooDerived::bar()\n"; }
};

int main()
{
   FooBase* pFoo = new FooDerived();
   pFoo->bar();

  return 0;
}

В этом примере компилятор наверняка знает, какой будет тип pFoo во время компиляции, поэтому ему не нужно использовать vptr для pFoo, верно? Есть ли более интересные случаи, когда компилятор может избежать использования vptr?


person zr.    schedule 25.02.2010    source источник


Ответы (4)


Основываясь на Ответ Эндрю Штейна, потому что я думаю, что вы также хотите знать, когда можно избежать так называемых «накладных расходов времени выполнения виртуальных функций». (Накладные расходы есть, но они незначительны, и о них редко стоит беспокоиться.)

Очень сложно избежать пробела указателя vtable, но сам указатель можно игнорировать, в том числе и в вашем примере. Поскольку инициализация pFoo находится в этой области, компилятор знает, что pFoo->bar должно означать FooDerived::bar, и ему не нужно проверять виртуальную таблицу. Существует также несколько методов кэширования, позволяющих избежать многократных просмотров vtable, от простых до сложных.

person Community    schedule 25.02.2010

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

Вы используете очень простую программу в одном файле.

Представьте, что у вас есть FooBase и FooDerived в Foo.h и Foo.cpp и main в main.cpp. Откуда компилятору при компиляции Foo.cpp знать, что во всей программе нет необходимости в vtbl. Он не видел main.cpp.

Окончательное определение может быть сделано только во время компоновки, когда уже слишком поздно и сложно изменять объектный файл, находить все вызовы sizeof(FooDerived) и т. д.

person Andrew Stein    schedule 25.02.2010
comment
Чтобы уточнить, не может быть нескольких версий (раскладок памяти) классов FooBase и FooDerived, или разные единицы компиляции могут быть несовместимы. Поскольку есть виртуальные функции, которые могут быть вызваны из нетривиально видимого базового указателя, компилятор должен выделить место для виртуальной таблицы, а это означает, что виртуальная таблица должна существовать во всех экземплярах объекта - даже в тривиальном в вашем примере. - person Mark B; 25.02.2010

Даже самые современные глобально оптимизирующие компиляторы по-прежнему придерживаются принципа «независимого перевода». В соответствии с этим принципом каждая единица перевода компилируется независимо, без каких-либо знаний о каких-либо других единицах перевода, которые могут существовать во всей конечной программе.

Когда вы объявляете класс с внешней связью, этот класс можно использовать в других единицах перевода. Компилятор понятия не имеет о том, как ваш класс может использоваться в этих других единицах перевода. Таким образом, он должен предположить, что каждый экземпляр этого класса может в общем случае нуждаться в том, чтобы его указатель виртуальной таблицы был правильно инициализирован в момент построения.

В вашем примере умный компилятор может выяснить, что динамический тип объекта *pFoo равен FooDerived. Это позволило бы компилятору оптимизировать код: генерировать прямой вызов функции FooDerived::bar в ответ на выражение pFoo->bar() (без использования виртуальной таблицы). Но, тем не менее, обычно компилятор должен правильно инициализировать указатель виртуальной таблицы в каждом объекте FooDerived.

person AnT    schedule 25.02.2010

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

Как уже говорили многие другие, если компилятор не выполняет оптимизацию всей программы/времени компоновки (становится все более популярным; GCC получает его в 4.5), ему все равно необходимо генерировать какую-то таблицу виртуальных функций для поддержки раздельной компиляции.

person Phil Miller    schedule 25.02.2010