Действительно ли встроенные виртуальные функции не имеют смысла?

У меня возник этот вопрос, когда я получил комментарий к обзору кода, говорящий, что виртуальные функции не обязательно должны быть встроенными.

Я думал, что встроенные виртуальные функции могут пригодиться в сценариях, где функции вызываются непосредственно на объекты. Но контр-аргумент пришел мне на ум: зачем нужно определять виртуальные, а затем использовать объекты для вызова методов?

Лучше ли использовать встроенные виртуальные функции, так как они почти никогда не расширяются?

Фрагмент кода, который я использовал для анализа:

class Temp
{
public:

    virtual ~Temp()
    {
    }
    virtual void myVirtualFunction() const
    {
        cout<<"Temp::myVirtualFunction"<<endl;
    }

};

class TempDerived : public Temp
{
public:

    void myVirtualFunction() const
    {
        cout<<"TempDerived::myVirtualFunction"<<endl;
    }

};

int main(void) 
{
    TempDerived aDerivedObj;
    //Compiler thinks it safe to expand the virtual functions
    aDerivedObj.myVirtualFunction();

    //type of object Temp points to is always known;
    //does compiler still expand virtual functions?
    //I doubt compiler would be this much intelligent!
    Temp* pTemp = &aDerivedObj;
    pTemp->myVirtualFunction();

    return 0;
}

Ответ 1

Виртуальные функции иногда могут быть встроены. Выдержка из превосходного С++ faq:

"Единственный раз, когда встроенный виртуальный вызов может быть встроено в компилятор знает" точный класс "объекта который является целью виртуального вызов функции. Это может произойти только когда компилятор имеет реальный объект а не указатель или ссылку на объект. I.e., либо с локальным объект, глобальный/статический объект или полностью содержащий объект внутри композит".

Ответ 2

С++ 11 добавил final. Это изменяет принятый ответ: больше не нужно знать точный класс объекта, достаточно знать, что у объекта есть, по крайней мере, тип класса, в котором функция была объявлена ​​окончательной:

class A { 
  virtual void foo();
};
class B : public A {
  inline virtual void foo() final { } 
};
class C : public B
{
};

void bar(B const& b) {
  A const& a = b; // Allowed, every B is an A.
  a.foo(); // Call to B::foo() can be inlined, even if b is actually a class C.
}

Ответ 3

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

class Base {
public:
  inline virtual ~Base () { }
};

class Derived1 : public Base {
  inline virtual ~Derived1 () { } // Implicitly calls Base::~Base ();
};

class Derived2 : public Derived1 {
  inline virtual ~Derived2 () { } // Implicitly calls Derived1::~Derived1 ();
};

void foo (Base * base) {
  delete base;             // Virtual call
}

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

Тот же принцип существует для конструкторов базового класса или для любого набора функций, где производная реализация также вызывает реализацию базовых классов.

Ответ 4

Я видел компиляторы, которые не испускают никакой v-таблицы, если вообще не существует встроенной функции (и определяется в одном файле реализации вместо заголовка). Они выдавали бы ошибки вроде missing vtable-for-class-A или что-то подобное, и вы были бы сбиты с толку, как я.

В самом деле, это не соответствует стандарту, но это происходит, поэтому рассмотрим возможность поместить хотя бы одну виртуальную функцию не в заголовок (если только виртуальный деструктор), чтобы компилятор мог выпустить vtable для класса в этом месте. Я знаю, что это происходит с некоторыми версиями gcc.

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

Однако компилятор не может полностью игнорировать inline. У него есть другая семантика, кроме ускорения вызова функции. Неявная встроенная для определения внутри класса - это механизм, который позволяет поместить определение в заголовок: только функции inline могут быть определены несколько раз по всей программе без нарушения каких-либо правил. В конце концов, он ведет себя так, как вы бы определили его только один раз во всей программе, даже если вы включили заголовок несколько раз в разные файлы, связанные друг с другом.

Ответ 5

Ну, фактически виртуальные функции всегда могут быть встроены, поскольку они статически связаны друг с другом: предположим, что у нас есть абстрактный класс Base с виртуальной функцией F и производными классами Derived1 и Derived2:

class Base {
  virtual void F() = 0;
};

class Derived1 : public Base {
  virtual void F();
};

class Derived2 : public Base {
  virtual void F();
};

Гипотетический вызов b->F();b типа Base*), очевидно, виртуальный. Но вы (или компилятор...) могли бы переписать его так же (предположим typeof является typeid -подобной функцией, которая возвращает значение, которое можно использовать в switch)

switch (typeof(b)) {
  case Derived1: b->Derived1::F(); break; // static, inlineable call
  case Derived2: b->Derived2::F(); break; // static, inlineable call
  case Base:     assert(!"pure virtual function call!");
  default:       b->F(); break; // virtual call (dyn-loaded code)
}

в то время как нам по-прежнему нужен RTTI для typeof, вызов может быть эффективно проиндексирован, в основном, встраиванием vtable внутри потока команд и специализированием вызова для всех вовлеченных классов. Это можно было бы также обобщить, специализируясь только на нескольких классах (скажем, просто Derived1):

switch (typeof(b)) {
  case Derived1: b->Derived1::F(); break; // hot path
  default:       b->F(); break; // default virtual call, cold path
}

Ответ 6

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

Ответ 7

Объявленные объявленные виртуальные функции встроены при вызове через объекты и игнорируются при вызове через указатель или ссылки.

Ответ 8

Маркировка виртуального метода inline помогает в дальнейшей оптимизации виртуальных функций в следующих двух случаях:

Ответ 9

С современными компиляторами это не повредит им. Некоторые древние компиляторы/компоновщики компоновщиков могли создать несколько vtables, но я не думаю, что это проблема.

Ответ 10

В тех случаях, когда вызов функции недвусмыслен, и функция является подходящим кандидатом для встраивания, компилятор достаточно умен, чтобы встроить код в любом случае.

Остальное время "встроенный виртуальный" - это вздор, и действительно, некоторые компиляторы не будут компилировать этот код.

Ответ 11

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

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

Ответ 12

Имеет смысл делать виртуальные функции, а затем называть их объектами, а не ссылками или указателями. Скотт Мейер рекомендует в своей книге "эффективный С++" никогда не переопределять унаследованную не виртуальную функцию. Это имеет смысл, потому что, когда вы создаете класс с не виртуальной функцией и переопределяете функцию в производном классе, вы можете быть уверены, что используете ее правильно самостоятельно, но не можете быть уверены, что другие будут использовать ее правильно. Кроме того, вы можете позже использовать его неправильно. Итак, если вы создаете функцию в базовом классе и хотите, чтобы ее можно было уточнить, вы должны сделать ее виртуальной. Если имеет смысл делать виртуальные функции и называть их объектами, имеет смысл также их встроить.