C ++ MythBusters。 インライン関数の神話

こんにちは。



この投票のおかげで、 このような強力な、しかしあまり使用されていないC ++言語に関するHabréの記事が十分にないことが判明しました。 C ++言語の高レベルの専門家、達人、魔術師、魔術師、およびこの言語を「背後に」置いて去った人は、これ以上読むことができません。 今日は、 比較的最近この言語を習得し始めたばかりの初心者、または(神が禁じている)数冊の本を読んで実際にすべてを学ぼうとする人に役立つように設計された一連の記事を始めたいと思います。



ここでの私の経験は明らかに不十分であるため、このような記事を書くためにできるだけ多くの著者を惹きつけたいと考えています。







叙情的な余談




この種の記事を結合するように設計されたタイトルについてのいくつかの言葉。 当然、偶然ではありませんでしたが、本質とはまったく一致していません。



私の記事は、すでにC ++言語に多少なりとも精通しているが、その上でプログラムを作成した経験がほとんどない人向けに設計されています。 「バケツ」、「カップ」、「オンス」などの精神でマニュアルや「入門」マニュアルを書くつもりはありません。 その代わりに、C ++言語の「狭い」部分を強調するようにします。 どういう意味? 私の例と他のプログラマーとのコミュニケーションの両方で、C ++言語の機能について正しいと確信している場合(時にはかなり長い時間)、私は何度も出会いましたが、その結果、それは深いことが判明しましたそして、よく知られている理由のために、神だけと取り返しのつかないほど誤解しています。



そして、理由は実際にはそれほど超自然的ではありません。 多くの場合、人的要因が役割を果たします。 たとえば、初心者向けの本を読んだ後、ご存知のように、多くのニュアンスが説明されておらず、言及されていないこともありますが、言語の基本の認識を単純化するために、読者 「私の意見では、これは論理的です」 「。 ここからは誤解の粒があり、時にはかなり重大な間違いにつながることもあり、ほとんどの場合、さまざまな種類のC ++競合の成功を妨げるだけです:)



だから最初の神話




ご存知のように、C ++ではインライン関数を宣言する可能性があります。 これは、インラインキーワードを使用して実現されます。 そのような関数の呼び出しの場所では、コンパイラーは呼び出しコマンドを生成せず(予備パラメーターがスタックにプッシュされます)、関数の本体を呼び出し場所にコピーします。対応するパラメーターを「その場で」置き換えます使用)。 当然、インラインは順序ではなくコンパイラへの推奨事項にすぎませんが、関数があまり複雑ではなく(かなり主観的な概念)、コードが関数のアドレスを取得するなどの操作を実行しない場合、ほとんどの場合コンパイラがそれを実行しますプログラマーを待っています。



置換された関数は非常に簡単に宣言されます:



inline void foo ( int & _i )

{

_i ++ ;

}








しかし、これは今ではそれについてではありません。 インラインクラスメソッドの使用を検討します。 そして、この神話が生じるかもしれない欠点を通して、小さな例から始めましょう。



クラスメソッド定義は、クラスの外側と内側の両方で記述でき、置換可能な関数も例外ではないことをご存知でしょ 。 さらに、クラス内で直接定義された関数は自動的に置換可能になるため、インラインキーワードは不要です。 例を考えてみてください(私はpublicを書かないためにクラスの代わりに構造体を使用しています)



// InlineTest.cpp



#include <cstdlib>

#include <iostream>



struct A

{

inline void foo ( ) { std :: cout << "A::foo()" << std :: endl ; }

} ;



struct B

{

inline void foo ( ) ;

} ;



void B :: foo ( )

{

std :: cout << "B::foo()" << std :: endl ;

}



int main ( )

{

A a ; B b ;

a. foo ( ) ;

b. foo ( ) ;

return EXIT_SUCCESS ;

}








この例では、すべてが正常であり、画面上に大切な行が表示されます。



A :: foo()

B :: foo()



さらに、コンパイラーは、呼び出しの代わりにメソッドの本体を実際に置き換えました。



最後に、今日の記事の最後に行きました。 問題は、(「良いプログラミングスタイル」に従って)クラスをcppファイルとhファイルに分割するときに始まります。



// Ah



#ifndef _A_H_

#define _A_H_



class A

{

public :

inline void foo ( ) ;

} ;



#endif // _A_H_








// A.cpp



#include "Ah"



#include <iostream>



void A :: foo ( )

{

std :: cout << "A::foo()" << std :: endl ;

}








// main.cpp



#include <cstdlib>

#include <iostream>

#include "Ah"



int main ( )

{

A a ;

a. foo ( ) ;



return EXIT_SUCCESS ;

}








リンクの段階で、次のようなエラーが表示されます(コンパイラに依存します-MSVCがあります)。



main.obj:エラーLNK2001:未解決の外部シンボル "public:void __thiscall A :: foo(void)"(?foo @ A @@ QAEXXZ)



なぜ?! すべてが非常に簡単です:置換可能なメソッドの定義とその呼び出しは、異なる翻訳単位にあります! これがどのように内部で正確に機能するかはよくわかりませんが、この問題は次のように見えます。



通常のメソッドの場合、翻訳ユニットmain.objにコンパイラーはXXXXXの呼び出しのようなものを配置し、後でリンカーはXXXXXを変換ユニットA.objからのメソッドA :: foo()の特定のアドレスに置き換えます(もちろん、Iすべてを簡素化しましたが、本質は変わりません)。



私たちの場合、インラインメソッドを扱っています。つまり、呼び出しの代わりに、コンパイラはメソッドのテキストを直接置き換える必要があります。 定義は別の翻訳単位にあるため、コンパイラはリンカの管理下にこの状況を残します。 ここには2つのポイントがあります。1つ目は「コンパイラーがメソッドの本体を置き換えるためにどれだけのスペースを残すべきか」、2つ目は、A :: foo()メソッドが変換ユニットA.objのどこでも使用されず、つまり、必要に応じて、コンパイラはメソッドの本体をコピーする必要があります)、したがって、このメソッドの別個のコンパイル済みバージョンは、最終的なオブジェクトファイルにまったく含まれません。



パラグラフ2をサポートするために、少し拡張した例を示します。



// Ah



#ifndef _A_H_

#define _A_H_



class A

{

public :

inline void foo ( ) ;

void bar ( ) ;

} ;



#endif // _A_H_








// A.cpp



#include "Ah"



#include <iostream>



void A :: foo ( )

{

std :: cout << "A::foo()" << std :: endl ;

}



void A :: bar ( )

{

std :: cout << "A::bar()" << std :: endl ;

foo ( ) ;

}








// main.cpp



#include <cstdlib>

#include <iostream>

#include "Ah"



int main ( )

{

A a ;

a. foo ( ) ;



return EXIT_SUCCESS ;

}








インラインメソッドA :: foo()が交換不可能なメソッドA :: bar()で呼び出されるため、すべてが正常に機能するようになりました。 最終的なバイナリのアセンブラコードを見ると、前述のように、foo()メソッドの個別のコンパイルされたバージョンがなく(つまり、メソッドに独自のアドレスがない)、メソッドの本体が呼び出しポイントに直接コピーされていることがわかります。



この状況から抜け出す方法は? 非常に簡単:ヘッダーファイルでインラインメソッドを直接定義する必要があります(必ずしもクラス宣言内である必要はありません)。 この場合、コンパイラはODR( One Definition Rule )エラーを無視するようリンカーに指示し、リンカーは結果のバイナリファイルに1つの定義のみを残すため、再定義エラーは発生しません



おわりに




少なくとも誰かが私の最初の記事が有用であり、C ++のような奇妙で時には矛盾しているが確かに興味深いプログラミング言語の完全な理解に役立つことを願っています。 頑張ってね:)



UPD。 gribozavrと通信する過程で、私の記事にODRに関する不正確さがありました。 斜体で強調表示。



All Articles