1つの定義ルール、インライン、およびそれらを組み合わせた場合の予期しない結果

C ++では、すべての関数を1回だけ定義する必要があります-1つの定義ルール、ODR。 異なる翻訳単位(.cppファイル)で同じ名前とシグネチャを持つ関数を定義するとすぐに、リンク段階でエラーが表示されます。



インライン関数は通常、ヘッダーファイル(.h)で定義されているため、すべての変換ユニットは関数の実装を確認し、呼び出しの場所でそれを置換できます。 したがって、このような機能を持つヘッダーファイルを複数のブロードキャストユニットに含めるとすぐに、ODRが正式に違反されますが、...エラーの兆候は表示されません。



これはなぜ、そしてどのような予期せぬ結果をもたらすのでしょうか?

理由-質問は比較的よく知られています( )。 一方で、上記の状況は実際的な理由で禁止することはできません-関数はそれを呼び出すすべての翻訳ユニットからアクセス可能である必要があります。そうでない場合、置換は不可能です。 一方、ODRは壊れており、これに対応するのは良いことです。



応答する方法は2つあります-エラーメッセージまたは無音です。 この特定のケースでは、リンカーは2番目を選択します。 リンカは、同じ名前と同じシグネチャを持つ複数のインライン関数を検出するとすぐに、それが同じ関数であると見なし、その裁量でそのうちの1つを選択します。



したがって、ODRの正式な違反は正式に排除されます。



突然、検出が困難な欠陥には広い範囲があります。 リンカが使用するポリシーは、関数が実際に同じであると想定しています。 同様に実装されます。 例が私たちを待っています-チップをストックして読み進めてください。



ゆっくりと攪拌し、小さなプリプロセッサを追加します(たとえば、これはATLのエラーハンドラーに対して行われます )。



 //CommonFile.h __declspec( noinline ) //  noinline inline void HandleErrorCondition( int condition ) { #ifndef OVERRIDE_STANDARD_HANDLING _exit(1); #else CustomHandleErrorCondition( condition ); #endif } //StaticLib.h #include <CommonFile.h> inline void SomeUsefulFunction() { //blahblahblah HandleErrorCondition( 0 ); } //StaticLib.cpp #include <StaticLib.h> blahblahblah,  SomeUsefulFunction() //Executable.cpp void CustomHandleErrorCondition( int condition ) { throw MyCustomException( condition ); } #define OVERRIDE_STANDARD_HANDLING #include <StaticLib.h> blahblahblah,  SomeUsefulFunction() //V2UncmUgaGlyaW5nIC0gd3d3LmFiYnl5LnJ1L3ZhY2FuY3k=
      
      









StaticLib.cppはStaticLib.lib静的ライブラリにコンパイルされ、Executable.cppは実行可能ファイル(.exeまたは.dll-とにかく)にコンパイルされ、StaticLib.libを静的にリンクします。



HandleErrorCondition()シグネチャには、__ declspec(noinline)が含まれています。これは、この関数の実装を置き換える必要がないことをコンパイラに伝えるVisual C ++属性です。 これは、コンパイラーが関数の実装を置換せず、実装を後で置き換えることができるように特に行われます。 Visual C ++に従います。



このキッチンが必要なトリッキーなプラン™は明らかです。すべてが開発者に合っている場合、デフォルトのハンドラーが使用されます。 デフォルトのハンドラーが適切でない場合は、独自のハンドラーを設定できます。



これは機能しますか? HandleErrorCondition()の実装-_exit()の呼び出しまたはCustomHandleErrorCondition()の呼び出し-が呼び出されますか?



不明



コンパイラがStaticLib.cppをコンパイルすると、最初の実装がオブジェクトファイル(StaticLib.obj)に含まれます-_exit()の呼び出しが含まれます。 コンパイラは、Executable.cppをコンパイルするときに、CustomHandleErrorCondition()を呼び出して、オブジェクトファイル(Executable.obj)に2番目の実装を含めます。



リンクすると、上記のODR違反状況が発生しますが、リンカーによって使用されるポリシーの観点からは同一の2つのインライン関数の実装が異なります。 リンカは1つのリンクを選択しますが、選択がリンク間で変更されないという事実はありません。



突然、計画どおりにプログラムが機能しなくなります。 特に素晴らしいのは、この例の動作は、エラーを処理する場合のみ異なります。 比較的まれな状況であり、確認を忘れないという事実ではありません。



説明されている動作(関数の1つを選択する)は、Visual C ++で実証されています。 一部の読者は、すでに苛性コメントを書く準備をしていますが、無駄です。 C ++ ISO / IEC 14882:2003(E)、パラグラフ3.2 / 5に従って、説明されている動作は定義されていません。 したがって、リンカーは、合理的な結果を提供する義務も、同じオブジェクトファイルを再リンクするときに同じ結果を提供する義務もありません。 場合によっては、振る舞いが期待したとおりになることもあれば、そうでないこともあります。 したがって、Visual C ++は無害です。



この例はかなり人工的で曲がっているように見えることに注意してください。 繰り返しますが、__ declspec(noinline)はVisual C ++固有の機能です。 まったく同じ状況に陥るには、他にも多くの方法があります。



たとえば、2つの異なるヘッダーで、同じ署名を持つインライン関数が誤って表示される場合があります。 両方のヘッダーが同じ.cppファイルに含まれる状況がない場合、コンパイルエラーは表示されず、ODR違反の状況に陥ります。 いくつかのコンパイラ設定の異なる値を使用して異なる.cppファイルをコンパイルすると、コードの動作が変更されます。 再び同じ状況。 #pragma packも貢献できます。



最後に、__ declspec(noinline)が存在しない場合、コンパイラは、対応する変換単位に指定されたコンパイラ設定およびプリプロセッサシンボルに対応する関数実装を置き換える場合があります。



この方向の欠陥の範囲は無限です。 そのような欠陥が既にコード内にある場合、そのような欠陥を検出することは非常に困難です。 プログラムの動作はリンク中に変化する場合があります。または、同じままである場合があります。したがって、欠陥を再現することさえ困難になる場合があります。



解決策は1つです。コードでインライン関数を使用する場合は、これらの関数の動作が変わらないように、異なる翻訳単位のコンパイルが実行されることを確認してください。 コンパイル設定は同じでなければならず、プリプロセッサシンボルは同じでなければなりません。



上記の場合、StaticLibプロジェクトでOVERRIDE_STANDARD_HANDLINGを定義して再構築する必要があります。



気をつけて。



ドミトリー・メッシェリャコフ

データ入力製品部門



All Articles