Могут ли виртуальные функции быть встроенными

Если я определю класс следующим образом:

class A{
public:
    A(){}
    virtual ~A(){}
    virtual void func(){}
};

Означает ли это, что виртуальный деструктор и func встроены

2 ответа

Решение

Выбор компилятором встроенной функции, которая определена как встроенная, полностью зависит от компилятора. В общем, virtual функции могут быть встроены, только когда компилятор может доказать, что статический тип соответствует динамическому типу, или когда компилятор может безопасно определить динамический тип. Например, когда вы используете значение типа A Компилятор знает, что динамический тип не может быть другим, и он может встроить функцию. При использовании указателя или ссылки компилятор обычно не может доказать, что статический тип одинаков и virtual функции обычно должны следовать обычной виртуальной диспетчеризации. Однако даже когда указатель используется, компилятор может иметь достаточно информации из контекста, чтобы знать точный динамический тип. Например, МатьеМ. дал следующий пример:

A* a = new B;
a->func();

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

Как правильно указал hvd, виртуальную диспетчеризацию можно обойти, вызвав виртуальную функцию полной квалификации, например, a->A::func(), в этом случае виртуальная функция также может быть встроена. Основная причина, по которой виртуальные функции, как правило, не встроены, заключается в необходимости выполнять виртуальную диспетчеризацию. Однако при полной квалификации вызываемая функция известна.

Да и несколькими способами. Вы можете увидеть некоторые примеры девиртуализации в этом письме, которое я отправил в список рассылки Clang около 2 лет назад.

Как и все оптимизации, это ожидает возможности компилятора устранить альтернативы: если он может доказать, что виртуальный вызов всегда разрешается в Derived::func тогда он может позвонить напрямую.

Существуют различные ситуации, давайте начнем с семантических доказательств:

  • SomeDerived& d где SomeDerived является final позволяет девиртуализации всех вызовов методов
  • SomeDerived& d, d.foo() где foo является final также позволяет девиртуализацию этого конкретного вызова

Затем возникают ситуации, когда вы знаете динамический тип объекта:

  • SomeDerived d; => динамический тип d обязательно SomeDerived
  • SomeDerived d; Base& b; => динамический тип b обязательно SomeDerived

Эти 4 ситуации девиртуализации обычно решаются интерфейсом компилятора, потому что они требуют фундаментальных знаний о семантике языка. Я могу засвидетельствовать, что все 4 реализованы в Clang, и я думаю, что они также реализованы в gcc.

Тем не менее, есть много ситуаций, когда это ломается:

struct Base { virtual void foo() = 0; };
struct Derived: Base { virtual void foo() { std::cout << "Hello, World!\n"; };

void opaque(Base& b);
void print(Base& b) { b.foo(); }

int main() {
    Derived d;

    opaque(d);

    print(d);
}

Хотя здесь очевидно, что призыв к foo решено Derived::fooClang/LLVM не оптимизирует его. Проблема в том, что:

  • Clang (front-end) не выполняет вставку, поэтому не может заменить print(d) от d.foo() и девиртуализировать звонок
  • LLVM (back-end) не знает семантику языка, поэтому даже после замены print(d) от d.foo() это предполагает, что виртуальный указатель d мог быть изменен opaque (чье определение непрозрачно, как следует из названия)

Я следил за усилиями в списках рассылки Clang и LLVM, поскольку оба набора разработчиков рассуждали о потере информации и о том, как заставить Clang сказать LLVM: "все в порядке", но, к сожалению, проблема нетривиальна и еще не решена... таким образом, недоработанная девиртуализация во внешнем интерфейсе, чтобы попытаться получить все очевидные случаи, а также некоторые не столь очевидные (хотя, по соглашению, внешний интерфейс не там, где вы их реализуете).


Для справки, код для девиртуализации в Clang можно найти в CGExprCXX.cpp в функции с именем canDevirtualizeMemberFunctionCalls, Это всего ~64 строки (прямо сейчас) и тщательно прокомментировано.

Другие вопросы по тегам