web-dev-qa-db-ja.com

2つの異なるcppファイルでインライングローバル関数を定義すると、なぜ魔法のような結果になるのですか?

2つの.cppファイル_file1.cpp_と_file2.cpp_があるとします。

_// file1.cpp
#include <iostream>

inline void foo()
{
    std::cout << "f1\n";
}

void f1()
{
    foo();
}
_

そして

_// file2.cpp
#include <iostream>

inline void foo()
{
   std::cout << "f2\n";
}

void f2()
{
    foo();
}
_

そして_main.cpp_でf1()f2()を前方宣言しました:

_void f1();
void f2();

int main()
{
    f1();
    f2();
}
_

結果(ビルドに依存しない、デバッグ/リリースビルドでも同じ結果):

_f1
f1
_

おっと:コンパイラはどういうわけか_file1.cpp_からの定義のみを選択し、f2()でも使用します。この動作の正確な説明は何ですか?.

inlinestaticに変更することがこの問題の解決策であることに注意してください。名前のない名前空間内にインライン定義を配置することでも問題が解決され、プログラムは次のように出力します。

_f1
f2
_
30
Narek Atayan

これは未定義の動作です。外部リンケージを持つ同じインライン関数の2つの定義は、いくつかの場所で定義できるオブジェクトのC++要件に違反するため、One Definition Ruleとして知られています。

3.2 1つの定義ルール

...

  1. クラス型(9節)、列挙型(7.2)、外部リンケージ付きインライン関数(7.1.2)、クラステンプレート(14節)、[...]の定義が複数ある場合、各定義は異なる翻訳単位に表示され、定義が以下の要件を満たしている場合に限ります。 Dという名前のエンティティが複数の翻訳単位で定義されている場合、

6.1 Dの各定義は、同じトークンのシーケンスで構成されるものとします。 [...]

これはstatic関数の問題ではありません。1つの定義ルールがそれらに適用されないためです。C++は、異なる変換単位で定義されたstatic関数を互いに独立していると見なします。

41
dasblinkenlight

コンパイラーは、標準でそう規定されているため、同じinline関数のすべての定義がすべての変換単位で同一であると想定する場合があります。したがって、必要な定義を選択できます。あなたの場合、それはたまたまf1

コンパイラが常に同じ定義を選択することに依存することはできないことに注意してください。前述の規則に違反すると、プログラムが不正な形式になります。コンパイラはそれを診断してエラーを出力することもできます。

関数がstaticまたは匿名の名前空間にある場合、fooという2つの異なる関数があり、コンパイラーは正しいファイルから1つを選択する必要があります。


参照用の関連標準:

インライン関数は、それがodrで使用されるすべての変換単位で定義されますすべてのケースで正確に同じ定義を持つ必要があります(3.2)。 [...]

N4141の7.1.2/4は私のものを強調しています。

31
Baum mit Augen

他の人が述べたように、コンパイラはC++標準に準拠しています1つの定義ルールは、関数がインラインである場合を除き、定義は同じ。

実際には、関数にインラインフラグが設定され、リンクステージでインラインフラグが設定されたトークンの複数の定義が実行されると、リンカーは1つを除いてすべてを静かに破棄します。インラインでフラグが立てられていないトークンの複数の定義に実行すると、代わりにエラーが生成されます。

このプロパティはinlineと呼ばれます。これは、LTO(リンク時最適化)の前に、関数の本体を取得して呼び出しサイトで「インライン化」するため、コンパイラーに関数の本体が必要であるためです。 inline関数をヘッダーファイルに入れ、各cppファイルで本文を確認し、コードを呼び出しサイトに「インライン化」できます。

これは、コードが実際にインライン化されることを意味するものではありません。むしろ、コンパイラーがインライン化しやすくなります。

ただし、重複を破棄する前に定義が同一であることを確認するコンパイラーについては知りません。これには、MSVCのCOMDATフォールディングなど、関数本体の定義が同一かどうかをチェックするコンパイラーが含まれます。本当に微妙なバグのセットなので、これは私を悲しくさせます。

問題を回避する適切な方法は、関数を匿名の名前空間に配置することです。一般に、匿名名前空間のソースファイルにeverythingを置くことを検討する必要があります。

これの別の本当に厄介な例:

_// A.cpp
struct Helper {
  std::vector<int> foo;
  Helper() {
    foo.reserve(100);
  }
};
// B.cpp
struct Helper {
  double x, y;
  Helper():x(0),y(0) {}
};
_

クラスの本体で定義されたメソッドは暗黙的にインラインです。 ODRルールが適用されます。ここには2つの異なるHelper::Helper()があり、どちらもインラインであり、それらは異なります。

2つのクラスのサイズは異なります。 1つのケースでは、2つのsizeof(double)を_0_で初期化します(ほとんどの状況で、ゼロフロートはゼロバイトであるため)。

別の方法では、最初にthreesizeof(void*)をゼロで初期化し、次にそれらのバイトで.reserve(100)を呼び出して、それらを次のように解釈しますベクトル。

リンク時に、これら2つの実装のうちの1つは破棄され、もう1つによって使用されます。さらに、どれが破棄されるかは、フルビルドではかなり確定的である可能性があります。部分的なビルドでは、順序が変わる可能性があります。

これで、完全なビルドでビルドして「正常に」動作するコードができましたが、部分的なビルドではメモリが破損します。また、makefile内のファイルの順序を変更すると、メモリが破損したり、libファイルがリンクされる順序が変更されたり、コンパイラがアップグレードされたりする可能性があります。

両方のcppファイルに、エクスポートするもの(完全修飾名前空間名を使用できるもの)を除くすべてを含む_namespace {}_ブロックがあった場合、これは起こりませんでした。

私はこのバグを正確に何度もキャッチしました。それがどれほど微妙であるかを考えると、その瞬間が襲いかかるのを待って、何回すり抜けたかはわかりません。