インライン仮想関数は本当にナンセンスなのでしょうか?
-
06-09-2019 - |
質問
私がこの質問を受け取ったのは、仮想関数をインラインにする必要はないというコード レビューのコメントを受け取ったときです。
インライン仮想関数は、関数がオブジェクトに対して直接呼び出されるシナリオで便利であると考えました。しかし、私の頭に浮かんだ反論は、なぜ仮想を定義してから、オブジェクトを使用してメソッドを呼び出す必要があるのかということです。
インライン仮想関数はいずれにせよほとんど展開されないため、使用しないのが最善でしょうか?
分析に使用したコードスニペット:
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 ++質問するからの抜粋:
」のみの時間インライン仮想コール インライン化することが可能である場合、コンパイラ オブジェクトの「正確なクラスを」知っています これは仮想の対象であります 関数呼び出し。これはのみ発生することができます コンパイラは、実際のオブジェクトを持っている場合 むしろへのポインタまたは参照より オブジェクト。すなわち、いずれかのローカルで オブジェクト、グローバル/静的オブジェクト、または 完全に内部のオブジェクトが含まれてい 複合。 "
他のヒント
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.
}
それはまだインラインそれらを持っていることは理にかなって仮想関数の一つのカテゴリがあります。以下のケースを考えてみます:
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
}
「基本」を削除するための呼び出しは、正しい派生クラスのデストラクタを呼び出すための仮想呼び出しを行いますが、この呼び出しがインライン化されていません。各デストラクタが呼び出されますので、しかし、それは親デストラクタは、(これらの場合には空である)ですが、コンパイラがインライン化することができます。のものをの呼び出し、彼らは事実上、基本クラスの関数を呼び出すことはありませんので、ます。
同じ原理は、基本クラスのコンストラクタまたは派生実装は、ベースクラスの実装を呼び出す関数の任意のセットのために存在します。
私は全く非インライン関数が存在しない(そして一個の実施ファイルの代わりに、ヘッダで定義された)場合に、任意のV-テーブルを放出しないコンパイラを見てきました。彼らはmissing vtable-for-class-A
または類似のもののようなエラーを投げるだろう、と私はあったように、あなたは、地獄と混同されるだろう。
実際のところ、それは標準に準拠していないのですが、コンパイラがその場所でクラスのvtableのを発することができるようにように、(唯一の仮想デストラクタ場合)ではないヘッダーに少なくとも1つの仮想機能を置くことを検討起こります。私はそれがgcc
の一部のバージョンで発生します知っています。
誰かが述べたように、インラインの仮想関数は、利益のの、時にはのことができますが、あなたは、のないのオブジェクトの動的な型を知っていますかとき、もちろん、ほとんどの場合、あなたはそれを使用します、それは最初の場所で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();
のb
有する)hypoteticalコール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)
}
我々はまだtypeof
のためのRTTIを必要としながら、、呼び出しが効果的に命令ストリーム内のvtableを埋め込み、関連するすべてのクラスのためにコールを専門に、基本的には、によってインライン化することができます。これはまた、唯一のいくつかのクラス(たとえば、単にDerived1
)を専門とすることにより、一般化することができます:
switch (typeof(b)) {
case Derived1: b->Derived1::F(); break; // hot path
default: b->F(); break; // default virtual call, cold path
}
仮想メソッドをインラインとしてマークすると、次の 2 つの場合に仮想関数をさらに最適化するのに役立ちます。
奇妙に繰り返されるテンプレート パターン (http://www.codeproject.com/Tips/537606/Cplusplus-Prefer-Curiously-Recurring-Template-Patt)
仮想メソッドをテンプレートに置き換える (http://www.di.unipi.it/~nids/docs/templates_vs_inheritance.html)
のインラインの本当に何もしない - それがヒントです。コンパイラはそれを無視するかもしれませんか、それは実装を見て、このアイデアを好きかどうかは、のインラインのせずにコールイベントをインライン化することがあります。コードの鮮明さが危機に瀕している場合は、のインラインの削除する必要があります。
インラインオブジェクトによって呼び出され、無視するとき、ポインタまたは参照を経由して呼び出されたときに、仮想関数がインライン化されている宣言ます。
最近のコンパイラには、それはそれらをinlibeするために、任意の害を行うことはありません。いくつかの古代のコンパイラ/リンカのコンボは、複数のvtableを作成しているかもしれませんが、私はそれはもう問題であるとは考えていない。
は、関数呼び出しをインライン化するのに適した候補明白であり、関数の場合では、コンパイラはとにかくコードをインライン化するのに十分スマートである。
残りの時間は「インライン仮想」はナンセンスであり、実際にいくつかのコンパイラは、そのコードをコンパイルしません。
コンパイラは関数をインラインすることができます。
仮想関数決定することができない、しかし、実行時に解決され、及びでコンパイルダイナミック型(と呼ばれることが関数の実装)を入力するのでので、コンパイラは、呼び出しをインライン化することができない。
これは、仮想関数を作るために意味をなさないし、その後のオブジェクトではなく、参照またはポインタにそれらを呼び出すん。スコット・マイヤーは、継承された非仮想関数を再定義しないように、彼の著書「効果的なC ++」で、推奨しています。あなたは非仮想関数を持つクラスを作成し、派生クラスの関数を再定義するとき、あなたが正しくそれを自分で使用してくださいかもしれないが、あなたは他の人がそれを正しく使用することを確認することができないので、それは、理にかなっています。また、後日yoruselfは間違ってそれを使用することができます。あなたは、基本クラス内の関数を作成し、あなたはそれがredifinableになりたいのであれば、あなたはそれが仮想にする必要があります。それは仮想関数を作成し、オブジェクトにそれらを呼び出すことは理にかなっている場合、それはまた、それらをインライン化することは理にかなっています。