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

172

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

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

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

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

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;
}
Aj.
источник
1
Подумайте о том, чтобы скомпилировать пример с любыми переключателями, которые вам нужны, чтобы получить список ассемблера, а затем показать рецензенту кода, что компилятор может встроить виртуальные функции.
Thomas L Holaday
1
Вышеупомянутое обычно не будет встроено, потому что вы вызываете виртуальную функцию для помощи базового класса. Хотя это зависит только от того, насколько умен компилятор. Если он сможет указать, что он pTemp->myVirtualFunction()может быть разрешен как не виртуальный вызов, он может иметь встроенный вызов. Этот ссылочный вызов встроен в g ++ 3.4.2: TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();ваш код - нет.
док
1
Gcc на самом деле делает сравнение записи vtable с конкретным символом, а затем использует встроенный вариант в цикле, если он совпадает. Это особенно полезно, если встроенная функция пуста, и в этом случае цикл можно исключить.
Саймон Рихтер
1
@doc Современный компилятор старается определить во время компиляции возможные значения указателей. Простого использования указателя недостаточно для предотвращения встраивания на любом значительном уровне оптимизации; GCC даже выполняет упрощения при нулевой оптимизации!
любопытный парень

Ответы:

153

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

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

ya23
источник
7
Верно, но стоит помнить, что компилятор может игнорировать встроенный спецификатор, даже если вызов может быть разрешен во время компиляции и может быть встроенным.
sharptooth
6
Другая ситуация, когда я думаю, что встраивание может произойти, это когда вы вызываете метод, например, как this-> Temp :: myVirtualFunction () - такой вызов пропускает разрешение виртуальной таблицы, и функция должна быть встроена без проблем - почему и если вы ' хочу сделать это - другая тема :)
RnR
5
@RnR. Нет необходимости иметь this->, достаточно просто использовать полное имя. И такое поведение имеет место для деструкторов, конструкторов и вообще для операторов присваивания (см. Мой ответ).
Ричард Корден
2
sharptooth - правда, но AFAIK это верно для всех встроенных функций, а не только для виртуальных встроенных функций.
Colen
2
void f (const Base & lhs, const Base & rhs) {} ------ При реализации функции вы никогда не знаете, на что указывают lhs и rhs до времени выполнения.
Байян Хуан
72

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.
}
MSalters
источник
Не смог встроить его в VS 2017.
Йола
1
Я не думаю, что это работает таким образом. Вызов foo () через указатель / ссылку типа A никогда не может быть встроенным. Вызов b.foo () должен разрешить вставку. Если вы не предполагаете, что компилятор уже знает, что это тип B, потому что он знает о предыдущей строке. Но это не типичное использование.
Джеффри Фауст
Например, сравните сгенерированный код для bar и bas здесь: godbolt.org/g/xy3rNh
Джеффри Фауст,
@JeffreyFaust Нет причины, по которой информация не должна распространяться, не так ли? И, iccкажется, сделать это, по этой ссылке.
Алексей Романов
@AlexeyRomanov У компиляторов есть свобода оптимизации вне стандарта, и, безусловно, есть! Для простых случаев, таких как выше, компилятор может знать тип и выполнять эту оптимизацию. Вещи редко бывают такими простыми, и не типично иметь возможность определять фактический тип полиморфной переменной во время компиляции. Я думаю, что OP заботится о «в целом», а не об этих особых случаях.
Джеффри Фауст
37

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

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

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

Ричард Корден
источник
23
Однако следует помнить, что пустые скобки не всегда означают, что деструктор ничего не делает. Деструкторы по умолчанию уничтожают каждый объект-член в классе, поэтому, если у вас есть несколько векторов в базовом классе, в этих пустых скобках может быть довольно много работы!
Филипп
14

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

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

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

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

Йоханнес Шауб - Литб
источник
11

Ну, на самом деле виртуальные функции всегда могут быть встроенными , если они статически связаны друг с другом: предположим, у нас есть абстрактный класс 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
}
CAFxX
источник
Это компиляторы, которые делают это? Или это просто домыслы? Извините, если я чрезмерно скептически настроен, но ваш тон в приведенном выше описании звучит примерно так: «они вполне могли бы сделать это!», Что отличается от «некоторых компиляторов»
Алекс Мейбург
Да, Graal выполняет полиморфную вставку (также для битового кода LLVM через Sulong)
CAFxX
4

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

Ченна Редди
источник
3

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

Sharptooth
источник
2
Для компиляторов, которые работают только с одиночными TU, они могут неявно включать только те функции, для которых у них есть определение. Функция может быть определена только в нескольких TU, если вы сделаете ее встроенной. 'inline' - это больше, чем подсказка, и он может значительно улучшить производительность при сборке g ++ / makefile.
Ричард Корден
3

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

tarachandverma
источник
1

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


источник
1

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

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

PaulJWilliams
источник
1
Когда вы вызываете метод базового класса из того же или производного класса, вызов однозначный и не виртуальный
sharptooth
1
@sharptooth: но тогда это будет не виртуальный встроенный метод. Компилятор может встроить функции, которые вы не просите, и он, вероятно, лучше знает, когда встроить или нет. Пусть это решит.
Дэвид Родригес - dribeas
1
@ Dribeas: Да, именно об этом я и говорю. Я только возразил против утверждения, что виртуальные вычисления разрешаются во время выполнения - это верно только тогда, когда вызов выполняется виртуально, а не для конкретного класса.
sharptooth
Я считаю, что это чепуха. Любая функция всегда может быть встроенной, независимо от ее размера, виртуальной или нет. Это зависит от того, как был написан компилятор. Если вы не согласны, то я ожидаю, что ваш компилятор также не может генерировать не встроенный код. То есть: компилятор может включать код, который во время выполнения тестирует на условия, которые он не может разрешить во время компиляции. Это как современные компиляторы могут разрешать постоянные значения / сокращать числовые выражения во время компиляции. Если функция / метод не является встроенной, это не означает, что она не может быть встроенной.
1

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

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

Лунная тень
источник
Какая версия g ++ не будет компилировать встроенные виртуалы?
Thomas L Holaday
Гектометр 4.1.1, который я сейчас здесь, кажется счастливым. Сначала я столкнулся с проблемами в этой кодовой базе, используя 4.0.x. Думаю, моя информация устарела, отредактировано.
лунная тень
0

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

Бальтазар
источник
0

На самом деле в некоторых случаях добавление «inline» к виртуальному окончательному переопределению может сделать ваш код некомпилируемым, поэтому иногда есть разница (по крайней мере, при использовании компилятора VS2017)!

На самом деле я делал виртуальную встроенную функцию окончательного переопределения в VS2017, добавляя стандарт c ++ 17 для компиляции и компоновки, и по какой-то причине это не удалось, когда я использую два проекта.

У меня был тестовый проект и библиотека реализации, которую я тестировал. В тестовом проекте у меня есть файл "linker_include.cpp", который #include * .cpp файлы из другого проекта, которые необходимы. Я знаю ... Я знаю, что могу настроить msbuild для использования объектных файлов из DLL, но имейте в виду, что это решение для Microsoft, в то время как включение файлов cpp не связано с системой сборки и намного проще для версии файл cpp, чем XML-файлы и настройки проекта и тому подобное ...

Интересно то, что я постоянно получал ошибку компоновщика из тестового проекта. Даже если я добавлю определение отсутствующих функций копией-вставкой, а не через include! Так странно. Другой проект создан, и между ними нет никакой связи, кроме маркировки ссылки на проект, поэтому существует порядок сборки, гарантирующий, что оба будут построены ...

Я думаю, что это какая-то ошибка в компиляторе. Я понятия не имею, существует ли он в компиляторе, поставляемом с VS2020, потому что я использую более старую версию, потому что некоторые SDK работают только с этим должным образом :-(

Я просто хотел добавить, что не только маркировка их как встроенных может что-то значить, но может даже заставить ваш код не собираться в некоторых редких случаях! Это странно, но приятно знать.

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

PS (Linux) .: Я ожидаю, что в gcc или clang не происходит то же самое, что я обычно делал для подобных вещей. Я не уверен, откуда возникла эта проблема ... Я предпочитаю делать c ++ на Linux или, по крайней мере, с некоторым gcc, но иногда проект отличается по потребностям.

предваренная
источник