仮想関数はインラインである必要はないというコードレビューのコメントを受け取ったときにこの質問を受けました。
インライン仮想関数は、関数がオブジェクトで直接呼び出されるシナリオで役立つと思いました。しかし、反論は私の頭に浮かびました-なぜ仮想を定義してからオブジェクトを使用してメソッドを呼び出したいのですか?
とにかく拡張されることはほとんどないので、インライン仮想関数を使用しないのが最善ですか?
分析に使用したコードスニペット:
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++ faq からの抜粋:
「インライン仮想呼び出しをインライン化できるのは、コンパイラが仮想関数呼び出しのターゲットであるオブジェクトの「正確なクラス」を知っているときだけです。これは、コンパイラがポインタではなく実際のオブジェクトを持っている場合にのみ発生しますオブジェクトへの参照。つまり、ローカルオブジェクト、グローバル/静的オブジェクト、またはコンポジット内の完全に含まれるオブジェクトのいずれか。
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.
}
仮想関数には1つのカテゴリがあり、それらをインラインにすると意味があります。以下の場合を考えてください:
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'を削除する呼び出しは、正しい派生クラスデストラクターを呼び出す仮想呼び出しを実行します。この呼び出しはインライン化されません。ただし、各デストラクタは親デストラクタ(これらの場合は空です)を呼び出すため、コンパイラは基本クラス関数を実質的に呼び出さないため、those呼び出しをインライン化できます。
同じ原則が、基本クラスコンストラクター、または派生実装が基本クラス実装も呼び出す関数のセットに存在します。
非インライン関数がまったく存在しない場合(およびヘッダーの代わりに1つの実装ファイルで定義されている場合)、vテーブルを出力しないコンパイラーを見てきました。それらはmissing vtable-for-class-A
などのエラーをスローし、私と同じようにあなたは地獄のように混乱します。
実際、それは標準に準拠していませんが、コンパイラがその場所のクラスのvtableを発行できるように、ヘッダーにない少なくとも1つの仮想関数(仮想デストラクタのみの場合)を配置することを検討してください。 gcc
のいくつかのバージョンで起こることを知っています。
誰かが言ったように、インライン仮想関数は利点になる可能性があります時々ですが、もちろん、ほとんどの場合、使用するときにそれを使用しますnotオブジェクトの動的な型を知っています。そもそも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();
(_Base*
_型のb
を使用)は明らかに仮想です。しかし、あなた(または コンパイラ ...)は、次のように書き換えることができます(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)
}
_
typeof
にはまだRTTIが必要ですが、基本的に、命令ストリーム内にvtableを埋め込み、関連するすべてのクラスの呼び出しを特化することにより、呼び出しを効果的にインライン化できます。これは、少数のクラス(たとえば_Derived1
_)のみを専門化することによって一般化することもできます。
_switch (typeof(b)) {
case Derived1: b->Derived1::F(); break; // hot path
default: b->F(); break; // default virtual call, cold path
}
_
インラインは実際には何もしません-これはヒントです。コンパイラは、それを無視するか、実装を確認してこのアイデアが気に入った場合、inlineなしで呼び出しイベントをインライン化します。コードの明瞭さが危うい場合は、inlineを削除する必要があります。
仮想メソッドをインラインでマークすると、次の2つのケースで仮想関数をさらに最適化できます。
不思議な繰り返しテンプレートパターン( http://www.codeproject.com/Tips/537606/Cplusplus-Prefer-Curiously-Recurring-Template-Patt )
仮想メソッドをテンプレートに置き換える( http://www.di.unipi.it/~nids/docs/templates_vs_inheritance.html )
インライン宣言された仮想関数は、オブジェクトを介して呼び出されるとインライン化され、ポインターまたは参照を介して呼び出されると無視されます。
最新のコンパイラーでは、それらを呼び出すのに害はありません。いくつかの古代のコンパイラ/リンカーのコンボが複数のvtableを作成した可能性がありますが、これはもう問題ではないと思います。
関数呼び出しが明確であり、関数がインライン化の適切な候補である場合、コンパイラはとにかくコードをインライン化するのに十分スマートです。
残りの「インライン仮想」はナンセンスであり、実際、一部のコンパイラはそのコードをコンパイルしません。
コンパイラは、コンパイル時に呼び出しを明確に解決できる場合にのみ関数をインライン化できます。
ただし、仮想関数は実行時に解決されるため、コンパイル型では動的型(したがって呼び出される関数実装)を決定できないため、コンパイラは呼び出しをインライン化できません。
仮想関数を作成し、参照またはポインターではなくオブジェクトでそれらを呼び出すことは理にかなっています。スコット・マイヤーは、彼の著書「効果的なc ++」で、継承された非仮想関数を決して再定義しないことを推奨しています。非仮想関数を使用してクラスを作成し、派生クラスで関数を再定義すると、自分で確実に正しく使用できるかもしれませんが、他の人がそれを正しく使用するかどうかはわかりません。また、後日、誤って使用することもあります。したがって、基本クラスで関数を作成し、それを再定義可能にしたい場合は、仮想にする必要があります。仮想関数を作成してオブジェクトで呼び出すことが理にかなっている場合は、それらをインライン化することも意味があります。