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

<p>я получил этот вопрос, когда получил комментарий к обзору кода, в котором говорилось, что виртуальные функции не должны быть встроенными.</p><p>я думал, что встроенные виртуальные функции могут пригодиться в сценариях, где функции вызываются непосредственно для объектов. но мне пришёл в голову контраргумент: зачем нужно определять виртуальные, а затем использовать объекты для вызова методов?</p><p>не лучше ли использовать встроенные виртуальные функции, поскольку они почти никогда не расширяются?</p><p>фрагмент кода, который я использовал для анализа:</p><pre><code>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; } </code></pre>

<p>виртуальные функции могут быть встроены иногда. отрывок из превосходного <a href="http://www.cs.technion.ac.il/users/yechiel/c++-faq/inline-virtuals.html">c++ faq</a> :</p><blockquote> <p>"единственный раз, когда встроенный виртуальный вызов может быть встроен, это когда компилятор знает" точный класс "объекта, который является целью вызова виртуальной функции. это может произойти только тогда, когда компилятор имеет фактический объект, а не указатель или ссылка на объект. т.е. либо с локальным объектом, глобальным/статическим объектом, либо с полностью заключенным объектом внутри композита. "</p> </blockquote>

<p>c++ 11 добавил <code>final</code>. это меняет принятый ответ: больше не нужно знать точный класс объекта, достаточно знать, что у объекта есть хотя бы тот тип класса, в котором функция была объявлена как финальная:</p><pre><code>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. } </code></pre>

<p>существует одна категория виртуальных функций, в которой все еще имеет смысл иметь встроенные функции. рассмотрим следующий случай:</p><pre><code>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 } </code></pre><p>вызов для удаления "base", будет выполнять виртуальный вызов для вызова правильного деструктора производного класса, этот вызов не является встроенным. однако, поскольку каждый деструктор вызывает свой родительский деструктор (который в этих случаях пуст), компилятор может встроить <em>те</em> вызовы, так как они не вызывают функции базового класса виртуально.</p><p>тот же принцип существует для конструкторов базовых классов или для любого набора функций, где производная реализация также вызывает реализацию базовых классов.</p>

<p>я видел компиляторы, которые не генерируют v-таблицу, если вообще не существует не встроенной функции (и определено в одном файле реализации вместо заголовка). они будут выдавать ошибки, такие как <code>missing vtable-for-class-a</code> или что-то подобное, и вы будете в замешательстве, как и я.</p><p>действительно, это не соответствует стандарту, но это происходит, поэтому рассмотрите возможность размещения хотя бы одной виртуальной функции вне заголовка (если не только виртуальный деструктор), чтобы компилятор мог создать vtable для класса в этом месте. я знаю, что это происходит с некоторыми версиями <code>gcc</code>.</p><p>как кто-то упоминал, встроенные виртуальные функции могут быть полезны <em>иногда</em>, но, конечно, чаще всего вы будете использовать его, когда вы <em>не</em> знаете динамический тип объекта, потому что это была вся причина для <code>virtual</code> в первую очередь.</p><p>однако компилятор не может полностью игнорировать <code>inline</code>. у этого есть другая семантика кроме ускорения вызова функции. <em>неявный inline</em> для определений в классе - это механизм, который позволяет поместить определение в заголовок: только функции <code>inline</code> могут быть определены несколько раз по всей программе без нарушения каких-либо правил. в конце концов, он ведет себя так, как вы бы определили его только один раз во всей программе, даже если вы включили заголовок несколько раз в разные файлы, связанные вместе.</p>

<p>ну, на самом деле <strong> виртуальные функции всегда могут быть встроенными </strong>, если они статически связаны друг с другом: предположим, у нас есть абстрактный класс <code>base</code> с виртуальной функцией <code>f</code> и производные классы <code>derived1</code> и <code>derived2</code>:</p><pre><code>class base { virtual void f() = 0; }; class derived1 : public base { virtual void f(); }; class derived2 : public base { virtual void f(); }; </code></pre><p>гипотетический вызов <code>b->f();</code> (с <code>b</code> типа <code>base*</code>), очевидно, является виртуальным. но вы (или <a href="https://hubicka.blogspot.jp/search/label/devirtualization">компилятор</a> ...) могли бы переписать его примерно так (предположим, что <code>typeof</code> - это <code>typeid</code>-подобная функция, которая возвращает значение, которое можно использовать в <code>switch</code>)</p><pre><code>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) } </code></pre><p>в то время как нам все еще нужен rtti для <code>typeof</code>, вызов может быть эффективно встроен путем, в основном, внедрения vtable в поток инструкций и специализации вызова для всех участвующих классов. это также можно обобщить, специализируя только несколько классов (скажем, просто <code>derived1</code>):</p><pre><code>switch (typeof(b)) { case derived1: b->derived1::f(); break; // hot path default: b->f(); break; // default virtual call, cold path } </code></pre>

<p><em>inline</em> на самом деле ничего не делает - это подсказка. компилятор может игнорировать его или встроить событие вызова без <em>inline</em>, если он видит реализацию и ей нравится эта идея. если на карту поставлена ясность кода, <em>inline</em> следует удалить.</p>

<p>маркировка виртуального метода в строке помогает в дальнейшей оптимизации виртуальных функций в следующих двух случаях:</p><ul> <li> <p>любопытно повторяющийся шаблон ( <a href="http://www.codeproject.com/tips/537606/cplusplus-prefer-curiously-recurring-template-patt">http://www.codeproject.com/tips/537606/cplusplus-prefer-curiously-recurring-template-patt</a> )</p> </li> <li> <p>замена виртуальных методов шаблонами ( <a href="http://www.di.unipi.it/~nids/docs/templates\_vs\_inheritance.html">http://www.di.unipi.it/~nids/docs/templates\_vs\_inheritance.html</a> )</p> </li> </ul>

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

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

<p>в тех случаях, когда вызов функции однозначен и функция является подходящим кандидатом для встраивания, компилятор достаточно умен, чтобы встроить код в любом случае.</p><p>в остальное время "встроенный виртуальный" - это нонсенс, и, действительно, некоторые компиляторы не будут компилировать этот код.</p>

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

<p>имеет смысл создавать виртуальные функции и затем вызывать их для объектов, а не для ссылок или указателей. скотт мейер рекомендует в своей книге "эффективный с ++" никогда не переопределять унаследованную не виртуальную функцию. это имеет смысл, потому что когда вы создаете класс с не-виртуальной функцией и переопределяете функцию в производном классе, вы можете быть уверены, что используете его правильно самостоятельно, но вы не можете быть уверены, что другие будут использовать его правильно. кроме того, вы можете позже использовать его неправильно. итак, если вы создаете функцию в базовом классе и хотите, чтобы она была перенаправляемой, вы должны сделать ее виртуальной. если имеет смысл создавать виртуальные функции и вызывать их для объектов, также имеет смысл встроить их.</p>