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

StackOverflow https://stackoverflow.com/questions/733737

  •  06-09-2019
  •  | 
  •  

Вопрос

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

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

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

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

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's 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;
}
Это было полезно?

Решение

Иногда виртуальные функции могут быть встроены.Отрывок из отличного Часто задаваемые вопросы по C++:

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

Другие советы

С++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.
}

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

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
}

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

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

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

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

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

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

А на самом деле виртуальные функции всегда могут быть встроены, если они статически связаны друг с другом:предположим, что у нас есть абстрактный класс 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, вызов может быть эффективно встроен путем встраивания виртуальной таблицы в поток команд и специализации вызова для всех задействованных классов.Это можно также обобщить, специализируя только на нескольких классах (скажем, только Derived1):

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

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

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

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

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

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

В остальное время «встроенный виртуальный» — это нонсенс, и некоторые компиляторы действительно не компилируют этот код.

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top