Действительно ли встроенные виртуальные функции не имеют смысла? - PullRequest
159 голосов
/ 09 апреля 2009

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

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

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

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

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;
}

Ответы [ 12 ]

138 голосов
/ 09 апреля 2009

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

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

69 голосов
/ 13 ноября 2013

C ++ 11 добавил final. Это меняет принятый ответ: больше не нужно знать точный класс объекта, достаточно знать, что у объекта есть хотя бы тот тип класса, в котором функция была объявлена ​​как 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.
}
36 голосов
/ 09 апреля 2009

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

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
}

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

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

14 голосов
/ 09 апреля 2009

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

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

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

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

10 голосов
/ 19 ноября 2011

Ну, на самом деле виртуальные функции всегда могут быть встроены , если они статически связаны друг с другом: предположим, у нас есть абстрактный класс 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
}
3 голосов
/ 15 сентября 2013

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

3 голосов
/ 01 августа 2011

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

3 голосов
/ 09 апреля 2009

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

1 голос
/ 09 апреля 2009

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

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

1 голос
/ 09 апреля 2009

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...