Может ли компилятор встроить виртуальную функцию, если я использую указатель в ясной ситуации?

Я уже читал Являются ли встроенные виртуальные функции действительно бессмысленными?. Но у меня все еще есть некоторые сомнения, и я не нашел ответа.

Говорят, что если ситуация не является двусмысленной, компилятор должен включить виртуальную функцию.

Однако:

Это может произойти только тогда, когда у компилятора есть фактический объект, а не указатель или ссылка на объект.

Итак, что, если у меня есть класс B, полученный из A one (который содержит функцию virtual void doSth()), и я использую указатель B*, а не A*:

B* b = new B;

b->doSth();
  • Предположим, что B не имеет дочерних классов. Это довольно очевидно (во время компиляции), какую функцию следует вызывать. Таким образом, можно быть встроенным. Это на самом деле?
  • Предположим, что B имеет несколько дочерних классов, но эти классы не имеют собственной функции doSth(). Поэтому компилятор должен "знать", что единственная функция для вызова - B::doSth(). Я предполагаю, что он не включен, хотя?

Ответ 1

Не имеет значения, имеет ли B любые производные классы. В этой ситуации B указывает на объект B, чтобы компилятор мог встроить вызов.

И, конечно, любой достойный современный компилятор может и будет делать это в вашей ситуации. Если вы не используете указатели, это становится намного проще. Тогда это не "оптимизация". Тот факт, что вы можете опустить виртуальный вызов, становится очевидным, если посмотреть только на AST node в левой части . -оператора. Но если вы используете указатели, вам нужно отслеживать динамический тип получателя. Но современные компиляторы способны на это.

EDIT: некоторый эксперимент в порядке.

// main1.cpp
struct A {
  virtual void f();
};

struct B : A { 
  virtual void f();
};

void g() {
  A *a = new A;
  a->f();

  a = new B;
  a->f();
}

// clang -O2 -S -emit-llvm -o - main1.cpp | c++filt
// ...
define void @g()() {
  %1 = tail call noalias i8* @operator new(unsigned int)(i32 4)
  %2 = bitcast i8* %1 to %struct.A*
  %3 = bitcast i8* %1 to i32 (...)***
  store i32 (...)** bitcast (i8** getelementptr inbounds ([3 x i8*]* @vtable for A, i32 0, i32 2) to i32 (...)**), i32 (...)*** %3, align 4
  tail call void @A::f()(%struct.A* %2)
  %4 = tail call noalias i8* @operator new(unsigned int)(i32 4)
  %5 = bitcast i8* %4 to i32 (...)***
  store i32 (...)** bitcast (i8** getelementptr inbounds ([3 x i8*]* @vtable for B, i32 0, i32 2) to i32 (...)**), i32 (...)*** %5, align 4
  %tmp = bitcast i8* %4 to %struct.B*
  tail call void @B::f()(%struct.B* %tmp)
  ret void
}
// ...

Как видно, clang выполняет прямые вызовы на f, причем, когда a указывает на a a и когда он указывает на a B. GCC делает это тоже.

Ответ 2

Функция виртуального члена может быть встроена, когда vtable не разыменован для вызова. Это можно сделать, сделав явный вызов для функции-члена.

class A
{
protected:

    int     a;
public:
    inline virtual void Func()
    {
        a = 0;
    }
};

class B : public A
{
public:
    inline virtual void Func()
    {
        a = 1;
    }
};

B   *obj = new B();

obj->Func();    //  Calls B::Func() through vtable;
obj->A::Func(); //  Inlines calls to A::Func();
obj->B::Func(); //  Inlines calls to B::Func();