Чи справді вбудовані віртуальні функції не мають сенсу?


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

1
Розглянемо компіляцію прикладу з будь-якими комутаторами, необхідними для отримання списку асемблерів, а потім покажіть переглядачу коду, що, дійсно, компілятор може вбудовувати віртуальні функції.
Thomas L Holaday

1
Зазначене вище не буде накреслено, тому що ви викликаєте віртуальну функцію за допомогою базового класу. Хоча це залежить лише від того, наскільки розумний компілятор. Якщо б вдалося вказати, що це pTemp->myVirtualFunction()може бути вирішено як невіртуальний виклик, він може мати вбудований виклик. Цей посилається на дзвінок позначається г ++ 3.4.2: TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();Ваш код - ні.
doc

1
Одне, що насправді gcc - це порівняти запис vtable з певним символом, а потім використовувати вбудований варіант у циклі, якщо він відповідає. Це особливо корисно, якщо вбудована функція порожня і цикл можна усунути в цьому випадку.
Саймон Ріхтер

1
@doc Сучасний компілятор дуже намагається визначити під час компіляції можливі значення покажчиків. Просто використання вказівника недостатньо для запобігання вбудовування на будь-якому значному рівні оптимізації; GCC навіть виконує спрощення при нульовій оптимізації!
curiousguy

Відповіді:


153

Віртуальні функції іноді можна накреслити. Уривок з відмінного C ++ на вмісті :

"Єдиний раз, коли вбудований віртуальний виклик може бути вбудований, це коли компілятор знає" точний клас "об'єкта, який є ціллю виклику віртуальної функції. Це може статися лише тоді, коли компілятор має фактичний об'єкт, а не покажчик або тобто посилання на об'єкт. Тобто, або з локальним об'єктом, глобальним / статичним об'єктом, або повністю об'єктом, що міститься всередині композиту. "


7
Правда, але варто пам’ятати, що компілятор вільний ігнорувати вбудований специфікатор, навіть якщо виклик може бути вирішений під час компіляції та може бути вбудований.
shartooth

6
Материнська ситуація, коли я думаю, що може виникнути інлінінг, це коли ви назвали метод, наприклад, як це-> Temp :: myVirtualFunction () - така виклик пропускає роздільну здатність віртуальної таблиці, і функцію слід без проблем вказувати - чому і якщо ви ' я хочу це зробити - це інша тема :)
RnR

5
@RnR. Не обов’язково мати "це->", достатньо лише використовувати кваліфіковане ім'я. І така поведінка має місце для деструкторів, конструкторів і взагалі для операторів призначення (див. Мою відповідь).
Річард Корден

2
shartooth - правда, але AFAIK це стосується всіх вбудованих функцій, а не лише віртуальних вбудованих функцій.
Колен

2
void f (const Base & lhs, const Base & rhs) {} ------ При здійсненні функції ви ніколи не знаєте, на що вказує lhs та rhs до часу виконання.
Baiyan Huang

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

Не вдалося вкласти його в VS 2017.
Yola

1
Я не думаю, що це працює так. Виклик foo () через вказівник / посилання типу A ніколи не можна накреслити. Виклик b.foo () повинен дозволяти вбудовуватися. Якщо ви не припускаєте, що компілятор вже знає, це тип B, оскільки він знає попередній рядок. Але це не типове використання.
Джеффрі Фауст

Наприклад, порівняйте створений код для бару та bas тут: godbolt.org/g/xy3rNh
Джеффрі Фауст

@JeffreyFaust Немає причини, щоб інформація не поширювалася, чи не так? І, iccздається, це робиться, за цим посиланням.
Олексій Романов

@AlexeyRomanov Укладачі мають свободу оптимізувати понад стандартні, і звичайно! У таких простих випадках, як вище, компілятор міг знати тип та здійснити цю оптимізацію. Речі рідко бувають такими простими, і не типово визначати фактичний тип поліморфної змінної під час компіляції. Я думаю, що ОП піклується про «взагалі», а не про ці особливі випадки.
Джеффрі Фауст

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чи чогось подібного, і ви б заплуталися як чорт, як і я.

Дійсно, це не відповідає стандарту, але трапляється так, що слід врахувати, як мінімум одну віртуальну функцію не в заголовку (якщо тільки віртуальний деструктор), щоб компілятор міг видавати vtable для класу в цьому місці. Я знаю, що це трапляється з деякими версіями 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-подобна функція, яка повертає значення, яке може бути використане в a 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
}

Чи є якісь компілятори, які роблять це? Або це лише спекуляція? Вибачте, якщо я надто скептично налаштований, але ваш тон у описі вище звучить приблизно так: "вони цілком могли це зробити!", Що відрізняється від "деякі компілятори роблять це".
Алекс Мейбург

Так, Graal робить поліморфне вбудовування (також для бітового коду LLVM через Sulong)
CAFxX

4

Позначення віртуального методу вбудованим, допомагає в подальшій оптимізації віртуальних функцій у наступних двох випадках:


3

inline насправді нічого не робить - це натяк. Компілятор може ігнорувати його, або він може вбудовувати події виклику без вбудованого, якщо він бачить реалізацію і сподобається цій ідеї. Якщо ясність коду під загрозою, то рядок слід видалити.


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

3

Вкладені оголошені віртуальні функції підкреслюються, коли викликаються через об'єкти, і ігноруються, коли викликаються через покажчик або посилання


1

З сучасними компіляторами, це не принесуть ніякої шкоди для того, щоб їх включити. Деякі стародавні комбінації компіляторів / лінкерів, можливо, створили декілька віртуальних файлів, але я вже не вірю, що це вже проблема.


1

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

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


1
Коли ви викликаєте метод базового класу з того ж або похідного класу, виклик є однозначним і невіртуальним
shartooth

1
@sharptooth: але тоді це буде невіртуальний вбудований метод. Компілятор може вбудовувати функції, про які ви не запитуєте, і він, мабуть, краще знає, коли вбудувати чи ні. Нехай це вирішить.
Девід Родрігес - дрибей

1
@dribeas: Так, саме про це я і говорю. Я заперечував лише проти твердження, що віртуальні фіксації вирішуються під час виконання - це справедливо лише тоді, коли виклик виконується фактично, а не для точного класу.
shartooth

Я вважаю, це нісенітниця. Будь-яка функція завжди може бути вбудованою, незалежно від того, наскільки вона велика, віртуальна чи ні. Це залежить від того, як був написаний укладач. Якщо ви не погоджуєтесь, то я очікую, що і ваш компілятор не може створити не вбудований код. Тобто: компілятор може включати код, який під час тестів виконує умови, які він не міг вирішити під час компіляції. Це так само, як сучасні компілятори можуть вирішувати постійні значення / зменшувати числові вирази під час компіляції. Якщо функція / метод не вбудована, це не означає, що вона не може бути вбудованою.

1

У випадках, коли виклик функції є однозначним, а функція - відповідним кандидатом для вбудованих даних, компілятор досить розумний, щоб все-таки вбудувати код.

Решту часу "вбудований віртуальний" - це нісенітниця, і справді деякі компілятори не збирають цей код.


Яка версія g ++ не збирає вбудовані віртуальні версії?
Thomas L Holaday

Гм. 4.1.1 Я зараз тут, здається, задоволений. Я вперше зіткнувся з проблемами з цією базою кодів за допомогою 4.0.x. Здогадайтесь, моя інформація застаріла, відредагована.
самогонник

0

Має сенс робити віртуальні функції, а потім викликати їх по об'єктах, а не посиланням або покажчикам. Скотт Мейєр рекомендує у своїй книзі "ефективний c ++" ніколи не переосмислювати успадковану невіртуальну функцію. Це має сенс, тому що, коли ви створюєте клас з невіртуальною функцією та переосмислюєте функцію у похідному класі, ви можете бути впевнені, що правильно ним користуєтесь, але ви не можете бути впевнені, що інші використовуватимуть його правильно. Крім того, ви можете пізніше використовувати його неправильно yoruself. Отже, якщо ви робите функцію в базовому класі, і хочете, щоб вона була зрозумілою, вам слід зробити її віртуальною. Якщо є сенс робити віртуальні функції та викликати їх на об'єктах, також є сенс вбудувати їх.


0

Насправді в деяких випадках додавання "inline" до віртуального остаточного заміщення може змусити ваш код не компілюватись, тому іноді є різниця (принаймні, під компілятором VS2017s)!

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

У мене був тестовий проект та реалізація DLL, що я є тестуванням. У тестовому проекті у мене є файл "linker_includes.cpp", який #include файли * .cpp з іншого проекту, які потрібні. Я знаю ... Я знаю, що я можу налаштувати msbuild для використання об’єктних файлів з DLL, але врахуйте, що це рішення, яке стосується мікрософт, в той час як файли cpp не мають відношення до збірної системи та набагато простіша версія файл cpp, ніж файли xml та налаштування проекту, і таке ...

Що було цікаво, це те, що я постійно отримував помилку лінкера від тестового проекту. Навіть якщо я додав визначення відсутніх функцій за допомогою копіювання пасти, а не через включення! Так дивно. Інший проект побудований, і між двома іншими, крім позначення посилання на проект, немає зв’язку, тому існує порядок побудови, щоб забезпечити те, що обидва завжди будуються ...

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

Я просто хотів додати, що не тільки маркування їх як вбудованого може щось означати, але може навіть змусити ваш код не побудувати в деяких рідкісних обставинах! Це дивно, але все ж добре знати.

PS: Код, над яким я працюю, пов'язаний з комп'ютерною графікою, тому я вважаю за краще вбудовування, і тому я використовував як заключний, так і вбудований. Я зберіг остаточний специфікатор, сподіваючись, що версія версії є достатньо розумною для створення DLL, додаючи її навіть без прямого натяку на мене, так ...

PS (Linux). Я очікую, що те саме не відбувається в gcc або clang, як я звичайно робив подібні речі. Я не впевнений, звідки ця проблема… Я вважаю за краще робити c ++ в Linux або принаймні з деякими gcc, але іноді проект відрізняється потребами.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.