Могут ли виртуальные функции быть встроенными
Если я определю класс следующим образом:
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::foo
Clang/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 строки (прямо сейчас) и тщательно прокомментировано.