1. ホーム
  2. c++

インクルードガードが再帰的インクルードや複数のシンボル定義を防いでくれないのはなぜですか?

2023-10-27 22:41:35

質問

に関する2つのよくある質問です。 ガード :

  1. 最初の質問です。

    なぜインクルードガードは私のヘッダーファイルを 相互、再帰的インクルージョン ? 私は以下のようなものを書くたびに、明らかにそこにあるのに存在しないシンボルについてのエラーや、さらに奇妙な構文エラーを受け続けています。

    "a.h"

    #ifndef A_H
    #define A_H
    
    #include "b.h"
    
    ...
    
    #endif // A_H
    
    

    "b.h"

    #ifndef B_H
    #define B_H
    
    #include "a.h"
    
    ...
    
    #endif // B_H
    
    

    "main.cpp"です。

    #include "a.h"
    int main()
    {
        ...
    }
    
    

    なぜ、"main.cpp" をコンパイルするとエラーが発生するのですか?解決するにはどうしたらよいですか?


  1. 2つ目の質問です。

    なぜインクルードガードは 多重定義 ? 例えば、私のプロジェクトが同じヘッダを含む2つのファイルを含んでいるとき、リンカは時々、あるシンボルが複数回定義されていることについて不平を言うことがあります。例えば

    "header.h"

    #ifndef HEADER_H
    #define HEADER_H
    
    int f()
    {
        return 0;
    }
    
    #endif // HEADER_H
    
    

    "source1.cpp"です。

    #include "header.h"
    ...
    
    

    "source2.cpp"です。

    #include "header.h"
    ...
    
    

    なぜこのようなことが起こるのでしょうか?問題を解決するためにはどうしたらよいのでしょうか?

どのように解決するのですか?

<ブロッククオート

最初の質問です。

なぜインクルードガードは私のヘッダーファイルを 相互、再帰的インクルージョン ?

彼らは .

彼らが役に立っていないのは 相互に含まれるヘッダ内のデータ構造の定義間の依存関係 . これが何を意味するのかを見るために、基本的なシナリオから始めて、なぜインクルードガードが相互包含の手助けになるのかを見てみましょう。

例えば、相互にインクルードする a.hb.h ヘッダーファイルはつまらない内容を持っています。つまり、質問のテキストからのコードセクションの省略は、空の文字列に置き換えられます。このような状況で、あなたの main.cpp は喜んでコンパイルされます。そして、これはあなたのインクルードガードのおかげです!

もし確信が持てないなら、それらを削除してみてください。

//================================================
// a.h

#include "b.h"

//================================================
// b.h

#include "a.h"

//================================================
// main.cpp
//
// Good luck getting this to compile...

#include "a.h"
int main()
{
    ...
}

コンパイラがインクルージョンの深さの制限に達すると、失敗を報告することに気がつくと思います。この制限は実装に依存します。C++11 標準の第 16.2/6 項による。

他のファイルの#include指示により読み込まれたソースファイルに#includeプリプロセッシング指示が表示される場合があります。 実装で定義された入れ子制限まで。 .

で、どうしたんですか? ?

  1. パースするとき main.cpp を解析するとき、プリプロセッサーはディレクティブ #include "a.h" . このディレクティブは、プリプロセッサにヘッダファイル a.h を処理し、その結果を取って、文字列 #include "a.h" をその結果で置き換える。
  2. 処理中に a.h を処理している間、プリプロセッサはディレクティブである #include "b.h" と同じメカニズムが適用されます: プリプロセッサはヘッダファイル b.h を処理し、その処理結果を受け取り、ヘッダファイル #include ディレクティブをその結果で置き換えます。
  3. を処理するとき b.h を処理する場合、ディレクティブ #include "a.h" は、プリプロセッサに a.h を処理し、そのディレクティブを結果で置き換えます。
  4. プリプロセッサーは a.h を再び解析し始め、その結果 #include "b.h" ディレクティブに再びぶつかり、無限に続く可能性のある再帰的な処理を設定することになります。重要なネストレベルに達すると、コンパイラはエラーを報告します。

インクルードガードが存在する場合 しかし、ステップ4で無限再帰が設定されることはありません。その理由を見てみましょう。

  1. ( 同上 ) パースするとき main.cpp を解析するとき、プリプロセッサーはディレクティブ #include "a.h" . これはプリプロセッサにヘッダファイル a.h という文字列を、その処理結果を受け取って、置き換えます。 #include "a.h" をその結果で置き換える。
  2. 処理中に a.h を処理している間、プリプロセッサはディレクティブである #ifndef A_H . マクロ A_H はまだ定義されていないので、次のテキストを処理し続けます。後続のディレクティブ( #defines A_H ) はマクロを定義します。 A_H . そして、プリプロセッサはディレクティブを満たします。 #include "b.h" : プリプロセッサは次にヘッダファイル b.h を処理し、その処理結果を受け取って #include ディレクティブをその結果で置き換えます。
  3. を処理するとき b.h を処理するとき、プリプロセッサーはディレクティブの #ifndef B_H . マクロ B_H はまだ定義されていないので、次のテキストを処理し続けます。後続のディレクティブ( #defines B_H ) はマクロを定義します。 B_H . 次に、ディレクティブ #include "a.h" を処理するようにプリプロセッサに伝えます。 a.h を処理し #include ディレクティブを b.h を前処理した結果 a.h ;
  4. コンパイラはプリプロセスを開始します a.h を再び開始し #ifndef A_H ディレクティブに再び出会います。しかし、以前の前処理で、マクロ A_H が定義されています。したがって、コンパイラは今回、以下のテキストをスキップして、マッチする #endif ディレクティブが見つかるまで次のテキストをスキップし、 この処理の出力は空文字列となります (何も #endif ディレクティブの後には何もないものとします)。したがって、プリプロセッサは #include "a.h" ディレクティブを b.h を空文字列で置き換えるまで、実行をトレースします。 #include ディレクティブを main.cpp .

このように インクルードガードは相互インクルードから保護する . しかし クラスの定義間の依存関係 には対応できません。

//================================================
// a.h

#ifndef A_H
#define A_H

#include "b.h"

struct A
{
};

#endif // A_H

//================================================
// b.h

#ifndef B_H
#define B_H

#include "a.h"

struct B
{
    A* pA;
};

#endif // B_H

//================================================
// main.cpp
//
// Good luck getting this to compile...

#include "a.h"
int main()
{
    ...
}

上記のヘッダを考えると main.cpp はコンパイルされません。

なぜこのようなことが起こるのでしょうか?

何が起こっているのかを見るには、もう一度1~4のステップを行えば十分です。

最初の 3 つのステップと 4 番目のステップの大部分は、この変更の影響を受けないことが簡単にわかります (納得するために読み進めてください)。しかし、ステップ 4 の終わりでは、何か違うことが起こります。 #include "a.h" ディレクティブを b.h を空文字列で指定すると、プリプロセッサは b.h の内容の解析を開始し、特に B . 残念ながら B はクラス A という、まさに今まで出会ったことのない というのは インクルードガードのためです!

事前に宣言されていない型のメンバ変数を宣言することは、もちろんエラーであり、コンパイラはそれを丁寧に指摘してくれます。

問題を解決するために必要なことは何でしょうか?

必要なのは 前方宣言 .

実際には 定義 クラスの A を定義するためには、クラス B というのは ポインタ への A のオブジェクトではなく、メンバ変数として宣言されています。 A . ポインターは固定サイズなので、コンパイラーは A を適切に定義するために、コンパイラは の正確なレイアウトを知る必要はありませんし、そのサイズを計算する必要もありません。 B . 従って,以下のようにすれば十分です. 前方宣言 クラス Ab.h を追加し、コンパイラにその存在を認識させます。

//================================================
// b.h

#ifndef B_H
#define B_H

// Forward declaration of A: no need to #include "a.h"
struct A;

struct B
{
    A* pA;
};

#endif // B_H

あなたの main.cpp はこれで確実にコンパイルされるでしょう。2、3の発言。

  1. を置き換えることで、相互包含を壊すだけでなく #include ディレクティブを前方宣言に置き換えるだけでなく b.h の依存関係を効果的に表現するのに十分でした。 B への A : 可能な限り/実用的な場合は前方宣言を使用することも 良いプログラミングの実践です。 は、不要なインクルージョンを避けることができるため、全体のコンパイル時間を短縮することができます。しかし、相互包含を排除した上で main.cpp は次のように修正されなければならない。 #include 両方とも a.hb.h (は、(後者が必要であれば)、なぜなら b.h はもう間接的に #include を通して a.h ;
  2. クラスの前方宣言が A へのポインタの参照は、コンパイラがそのクラスへのポインタを宣言するのに十分ですが(あるいは不完全な型が許容される他のコンテキストでそれを使用するのに十分です)。 A へのポインタの参照 (例えばメンバ関数を呼び出すため) やサイズの計算は 違法 の操作は不完全な型に対するものです。もしそれが必要なら、完全な定義の A の完全な定義をコンパイラが利用できるようにする必要があります。 つまり、それを定義するヘッダーファイルが含まれていなければなりません。このため、クラスの定義とそのメンバ関数の実装は通常、ヘッダファイルとそのクラスの実装ファイルに分割されます(クラス テンプレート はこのルールの例外です):実装ファイル、これは決して #include になることのない実装ファイルは、安全に #include を使用することで、必要なすべてのヘッダを安全に定義することができます。一方、ヘッダーファイルは #include 他のヘッダーファイル がなければ の定義が本当に必要でない限り、そうする必要があります。 基底クラス を可視化するため)、可能かつ実用的であればいつでも前方宣言を使用します。

2つ目の質問です。

なぜインクルードガードは 多重定義 ?

彼らは .

このような場合、複数の定義からあなたを保護することはできません。 を別々の翻訳ユニットで . これについても この Q&A をStackOverflowでご覧ください。

これを見るには、インクルードガードを削除して、次のように修正したバージョンをコンパイルしてみてください。 source1.cpp (または source2.cpp でもよい)。

//================================================
// source1.cpp
//
// Good luck getting this to compile...

#include "header.h"
#include "header.h"

int main()
{
    ...
}

コンパイラはここで確実に f() が再定義されていることに文句を言うでしょう。それは明らかで、その定義が二度含まれているのです! しかし、上記の source1.cpp は問題なくコンパイルされます。 header.h が適切なインクルードガードを含んでいる場合 . それは期待されています。

それでも、インクルードガードが存在し、コンパイラがエラーメッセージで悩まなくなるときでも リンカ のコンパイルで得られたオブジェクトコードをマージする際に、複数の定義が見つかったという事実を主張するでしょう。 source1.cppsource2.cpp であるため、実行ファイルの生成は拒否されます。

<ブロッククオート

なぜこのようなことが起こるのでしょうか?

基本的に、各 .cpp ファイル (この文脈での専門用語は 翻訳ユニット ) は別々にコンパイルされ は独立して . をパースするとき .cpp ファイルを解析するとき、プリプロセッサはすべての #include ディレクティブを処理し、遭遇したすべてのマクロを展開します。この純粋なテキスト処理の出力は、オブジェクトコードに変換するためにコンパイラの入力として与えられます。コンパイラは1つの翻訳ユニットのオブジェクトコードを生成し終わると、次の翻訳ユニットに進み、前の翻訳ユニットを処理している間に遭遇したすべてのマクロ定義は忘れ去られます。

実際、プロジェクトを n 翻訳ユニット ( .cpp ファイル)を実行するようなもので、同じプログラム(コンパイラ)を n 回、毎回異なる入力で実行するようなものです。 は前のプログラムの実行の状態を共有しません。 . したがって、各翻訳は独立して実行され、ある翻訳ユニットのコンパイル時に遭遇したプリプロセッサシンボルは、他の翻訳ユニットのコンパイル時には記憶されません(少し考えれば、これが実際に望ましい動作であることに容易に気がつくでしょう)。

したがって、インクルードガードが再帰的な相互包含と 冗長 に含まれているかどうかを検出することはできません。 異なる に含まれているかどうかを検出することはできません。

をコンパイルして生成されたオブジェクトコードをマージする場合、すべての .cpp ファイルをマージする際、リンカは は同じシンボルが複数回定義されていることを認識し、これは 一回の定義ルール . C++11 標準の第 3.2/3 項による。

すべてのプログラムには、すべての 非インライン 関数や変数が含まれていなければなりません。定義はプログラム中に明示的に現れるか、標準ライブラリやユーザ定義ライブラリにあるか、 (適切な場合には) 暗黙的に定義されます (12.1, 12.4, 12.8 を参照)。 インライン関数は、それが使用されるすべての翻訳単位で定義されなければならない。 .

したがって、リンカはエラーを出し、あなたのプログラムの実行ファイルの生成を拒否します。

<ブロッククオート

問題を解決するために必要なことは何ですか?

もし であるヘッダファイル内に関数定義を保持したい場合、そのヘッダファイルは #include によって 複数の 翻訳ユニット(ヘッダーが #include である場合、問題は発生しません。 一つ の翻訳ユニット)を使用する場合は inline キーワードを使う必要があります。

それ以外の場合は 宣言 にある関数の header.h その定義 (本体) を 1 別の .cpp ファイルのみを作成します (これは古典的な方法です)。

inline キーワードはコンパイラに対して、通常の関数呼び出しのためのスタックフレームを設定するのではなく、呼び出し先で直接関数本体をインライン化するようにという拘束力のない要求を表します。コンパイラはあなたの要求を満たす必要はありませんが inline キーワードはリンカに複数のシンボル定義を許容するように指示することに成功します。C++11 標準の第 3.2/5 項によると。

の定義は 1 つ以上あることがあります。 クラス型(第9項)、列挙型(第7.2項)があります。 外部リンクによるインライン関数 (7.1.2), クラステンプレート (第14節), 非静的関数テンプレート (14.5.6), クラステンプレートの静的データメンバー (14.5.1.3), クラステンプレートのメンバー関数 (14.5.1.1), 又はいくつかのテンプレートパラメータが指定されていないテンプレート特殊化 (14.7, 14.5.5) プログラムにおいて、それぞれの定義が異なる翻訳単位に現れ、その定義が以下の要求を満たしている場合に限る [...]...。

上記のパラグラフは、基本的にヘッダファイルによくある定義をすべてリストアップしています。 なぜなら、それらは安全に複数の翻訳ユニットに含めることができるからです。外部リンクのある他のすべての定義は、代わりにソースファイルに属します。

を使うことで static キーワードの代わりに inline キーワードはリンカーエラーを抑制することにもつながります。 内部リンク このため、各翻訳ユニットにはプライベートな コピー を保持することになります。しかし、これは最終的に実行ファイルのサイズを大きくすることになり inline の使用が一般的に好まれます。

と同じ結果を得るための代替方法として static キーワードと同じ結果を得るには、関数 f() の中に 無名名前空間 . C++11 標準の第 3.5/4 項による。

無名名前空間、または無名名前空間内で直接または間接的に宣言された名前空間は、内部連結を持ちます。他のすべての名前空間は外部連結を持つ。上記の内部連結を与えられていない名前空間スコープを持つ名前は、その名前である場合、包含する名前空間と同じ連結を持つ。

- 変数、または

- 関数 または

- 名前付きクラス (第9項)、またはリンクの目的でクラスが typedef 名を持つ typedef 宣言で定義された無名クラス (第7.1.3項)、あるいは

- 名前付き列挙(7.2)、または列挙がリンク目的のために typedef 名を持つ typedef 宣言で定義された無名列挙(7.1.3)、または

- リンケージを持つ列挙体に属する列挙体、または

- テンプレート。

上述と同じ理由で inline のキーワードが優先されるべきです。