インライン仮想関数は本当にナンセンスですか?


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
アセンブラーのリストを取得するために必要なスイッチを使用して例をコンパイルし、コンパイラーが実際に仮想関数をインライン化できることをコードレビューアーに示すことを検討してください。
トーマスLホラディ

1
基本クラスを使用して仮想関数を呼び出しているので、上記は通常インライン化されません。それはコンパイラがどれだけ賢いかに依存しますが。pTemp->myVirtualFunction()非仮想呼び出しとして解決できる可能性があることを指摘できる場合は、その呼び出しをインライン化している可能性があります。この参照される呼び出しは、g ++ 3.4.2によってインライン化されTempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();ます。コードはインライン化されません。
2010

1
gccが実際に行うことの1つは、vtableエントリを特定のシンボルと比較し、一致する場合はループ内でインラインバリアントを使用することです。これは、インライン化された関数が空であり、この場合ループを削除できる場合に特に便利です。
Simon Richter

1
@doc現代のコンパイラは、コンパイル時にポインタの可能な値を決定しようとします。ポインタを使用するだけでは、重要な最適化レベルでのインライン化を防ぐには不十分です。GCCは最適化ゼロで単純化も実行します!
curiousguy

回答:


153

仮想関数は時々インライン化できます。優れたC ++のFAQからの抜粋:

「インライン仮想呼び出しをインライン化できるのは、コンパイラーが仮想関数呼び出しのターゲットであるオブジェクトの「正確なクラス」を知っているときだけです。これは、コンパイラーがポインターではなく実際のオブジェクトを持っているか、オブジェクトへの参照です。つまり、ローカルオブジェクト、グローバル/静的オブジェクト、またはコンポジット内に完全に含まれているオブジェクトのいずれかです。」


7
正しいですが、呼び出しがコンパイル時に解決されてインライン化できる場合でも、コンパイラーはインライン指定子を自由に無視できることを覚えておく価値があります。
シャープトゥース

6
インライン化が発生する可能性があると私が思う別の状況は、たとえば次のようにメソッドを呼び出すときです-> Temp :: myVirtualFunction()-このような呼び出しは仮想テーブルの解決をスキップし、関数は問題なくインライン化されます-理由と理由dやりたいことは別のトピックです:)
RnR

5
@RnR。'this->'を付ける必要はありません。修飾名を使用するだけで十分です。そしてこの振る舞いは、デストラクタ、コンストラクタ、そして一般に代入演算子に対して起こります(私の答えを参照してください)。
Richard Corden

2
sharptooth-trueですが、これは仮想インライン関数だけでなく、すべてのインライン関数にも当てはまります。
コレン

2
void f(const Base&lhs、const Base&rhs){} ------関数の実装では、実行時までlhsとrhsが何を指しているのかは決してわかりません。
Baiyan Huang 2010

72

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

VS 2017でそれをインライン化することができませんでした
Yola

1
私はそれがこのように機能するとは思わない。タイプAのポインター/参照を介したfoo()の呼び出しは、インライン化できません。b.foo()を呼び出すと、インライン化が可能になります。前の行を認識しているので、コンパイラがこれをタイプBであることをすでに知っていることを示唆しているのでない限り。しかし、それは典型的な使用法ではありません。
ジェフリーファウスト

たとえば、ここでbarとbasに対して生成されたコードを比較します。godbolt.org
Jeffrey Faust

@JeffreyFaust情報が伝達されるべきではない理由はありませんか?そして、iccそのリンクに従って、それを行うようです。
Alexey Romanov

@AlexeyRomanovコンパイラには、標準を超えて最適化する自由があり、確かにそうです!上記のような単純なケースでは、コンパイラーはタイプを認識し、この最適化を行うことができます。物事がこれほど単純なことはめったになく、コンパイル時にポリモーフィック変数の実際のタイプを判別できるのは一般的ではありません。OPは「一般的な」ことを考慮し、これらの特殊なケースは考慮しないと思います。
ジェフリーファウスト

37

仮想関数の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'を削除する呼び出しは、正しい派生クラスデストラクタを呼び出す仮想呼び出しを実行します。この呼び出しはインライン化されません。ただし、各デストラクタは親デストラクタ(これらの場合は空)を呼び出すため、基本クラス関数を仮想的に呼び出さないため、コンパイラはそれらの呼び出しをインライン化できます。

同じ原則が、基本クラスコンストラクターまたは派生した実装も基本クラスの実装を呼び出す関数のセットに存在します。


23
中括弧が空であっても必ずしもデストラクタが何もしないことを意味するわけではありません。デストラクタは、クラスのすべてのメンバーオブジェクトをデフォルトで破棄します。そのため、基本クラスにいくつかのベクトルがある場合、これらの空のブレースでかなりの作業になる可能性があります。
フィリップ

14

非インライン関数がまったく存在しない場合(そしてヘッダーの代わりに1つの実装ファイルで定義されている場合)、v-tableを発行しないコンパイラーを見てきました。それらは同様missing vtable-for-class-Aまたは類似のエラーをスローし、私がそうであったように、あなたは地獄と混同されます。

実際、これは標準に準拠していませんが、ヘッダーに(仮想デストラクタのみの場合は)少なくとも1つの仮想関数を配置して、コンパイラがその場所でクラスの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中で使用できる値を返す様機能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
}

これを行うコンパイラはありますか?それともこれは単なる憶測ですか?私が過度に懐疑的である場合は申し訳ありませんが、上記の説明でのあなたの口調は、「一部のコンパイラがこれを行う」とは異なり、「彼らは完全にこれを行うことができます!」のように聞こえます。
Alex Meiburg

はい、Graalはポリモーフィックインライン化を行います(Sulongを介したLLVMビットコードも同様)
CAFxX


3

inlineは実際には何もしません-それはヒントです。コンパイラはそれを無視するか、実装を見てこのアイデアが気に入った場合、インラインなしで呼び出しイベントをインライン化する可能性があります。コードの明瞭さが問題となっている場合は、インラインを削除する必要があります。


2
単一のTUでのみ動作するコンパイラーの場合、それらは暗黙的に定義されている関数のみをインライン化できます。関数は、インラインにした場合にのみ、複数のTUで定義できます。「インライン」はヒント以上のものであり、g ++ / makefileビルドのパフォーマンスを劇的に向上させることができます。
Richard Corden

3

インライン宣言された仮想関数は、オブジェクトを介して呼び出されるとインライン化され、ポインターまたは参照を介して呼び出されると無視されます。


1

最新のコンパイラでは、それらを組み込むことは何の害もありません。いくつかの古代のコンパイラ/リンカーのコンボは複数のvtableを作成した可能性がありますが、それが問題であるとはもう信じていません。


1

コンパイラーは、コンパイル時に呼び出しを明確に解決できる場合にのみ、関数をインライン化できます。

ただし、仮想関数は実行時に解決されるため、コンパイルタイプでは動的タイプ(したがって呼び出される関数の実装)を決定できないため、コンパイラーは呼び出しをインライン化できません。


1
同じクラスまたは派生クラスから基本クラスメソッドを呼び出す場合、その呼び出しは明確で非仮想です
シャープトゥース

1
@sharptooth:しかし、それは非仮想インラインメソッドになります。コンパイラーは、要求しない関数をインライン化できます。インライン化するかどうかは、おそらくよくわかっています。それを決めましょう。
デビッドロドリゲス-ドリベス

1
@dribeas:はい、それはまさに私が話していることです。私は、仮想機能が実行時に解決されるという声明に反対しました-これは、正確なクラスではなく、仮想的に呼び出しが行われた場合にのみ当てはまります。
シャープトゥース2009

それはナンセンスだと思います。関数の大きさや仮想関数であるかどうかにかかわらず、関数は常にインライン化できます。コンパイラーの作成方法によって異なります。同意しない場合は、コンパイラがインライン化されていないコードも生成できないと思います。つまり、コンパイラーは、コンパイル時に解決できなかった条件を実行時にテストするコードを含めることができます。それは、現代のコンパイラーがコンパイル時に定数値を解決したり、数値式を減らしたりできるのと同じです。関数/メソッドがインライン化されていなくても、インライン化できないわけではありません。

1

関数呼び出しが明確であり、関数がインライン化に適した候補である場合、コンパイラーはとにかくコードをインライン化するのに十分スマートです。

残りの「インライン仮想」はナンセンスであり、実際に一部のコンパイラはそのコードをコンパイルしません。


どのバージョンのg ++​​がインラインバーチャルをコンパイルしませんか?
トーマスL

うーん。私がここにいる4.1.1は今や幸せそうです。4.0.xを使用してこのコードベースで最初に問題が発生しました。私の情報が古く、編集されていると思います。
moonshadow

0

仮想関数を作成し、参照やポインタではなくオブジェクトで呼び出すことは理にかなっています。Scott Meyerは、彼の著書「効果的なc ++」で、継承された非仮想関数を再定義しないことを推奨しています。これは理にかなっています。非仮想関数を使用してクラスを作成し、派生クラスで関数を再定義すると、自分で正しく使用できることは確かですが、他の人が正しく使用できるとは限らないためです。また、後日、誤って自分自身で使用することがあります。したがって、基本クラスで関数を作成し、それを再定義可能にする場合は、仮想関数にする必要があります。仮想関数を作成してオブジェクトで呼び出すことが理にかなっている場合は、それらをインライン化することも理にかなっています。


0

実際、いくつかのケースでは、仮想最終オーバーライドに「インライン」を追加すると、コードがコンパイルされない可能性があるため、(少なくともVS2017sコンパイラでは)違いがある場合があります。

実際、私はVS2017で仮想インライン最終オーバーライド関数を実行していて、コンパイルとリンクにc ++ 17標準を追加しており、2つのプロジェクトを使用しているときに何らかの理由で失敗しました。

私は単体テストのテストプロジェクトと実装DLLを持っていました。テストプロジェクトでは、必要な他のプロジェクトの* .cppファイルを#includeする「linker_includes.cpp」ファイルを使用しています。DLLのオブジェクトファイルを使用するようにmsbuildを設定できることはわかっていますが、cppファイルを含めることはビルドシステムとは無関係であり、バージョン管理がはるかに簡単である一方で、Microsoft固有のソリューションであることを覚えておいてくださいXMLファイルやプロジェクト設定などよりもcppファイル...

興味深いのは、テストプロジェクトから常にリンカーエラーが発生していたことです。不足している関数の定義をインクルードではなくコピーペーストで追加した場合でも、とても奇妙。他のプロジェクトがビルドされており、プロジェクト参照をマークする以外は2つの間に接続がないため、両方が常にビルドされるようにするビルド順序があります...

コンパイラのバグだと思います。VS2020に同梱されているコンパイラに存在するかどうかはわかりません。古いバージョンを使用しているため、一部のSDKは適切にしか機能しないためです:-(

インラインとしてマークするだけで意味がないだけでなく、まれな状況でコードがビルドされない可能性があることを追加したいと思います。これは奇妙ですが、知っておくと良いでしょう。

PS .:私が取り組んでいるコードはコンピュータグラフィックスに関連しているので、インライン化を好むので、ファイナルとインラインの両方を使用しました。私は最後の指定子を保持して、リリースビルドがDLLをインライン化するのに十分なほどスマートであることを願っています。

PS(Linux):こういったことを日常的に行っていたので、gccやclangでも同じことが起こらないと思います。この問題の原因がわからない... Linuxでc ++を実行するか、少なくとも一部のgccを使用することを好みますが、プロジェクトのニーズが異なる場合があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.