最初の質問:
相互の再帰的インクルードからヘッダー ファイルを保護するインクルード ガードがないのはなぜですか?
彼らはです。
彼らが助けていないのは、相互にヘッダーを含むデータ構造の定義間の依存関係です。これが何を意味するかを理解するために、基本的なシナリオから始めて、インクルード ガードが相互包含に役立つ理由を見てみましょう。
相互インクルード ファイルa.h
とb.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 ディレクティブのために読み取られたソース ファイルに、実装で定義された入れ子制限まで表示される場合があります。
それで、何が起こっているのですか?
- 解析時
main.cpp
に、プリプロセッサはディレクティブを満たし#include "a.h"
ます。このディレクティブは、ヘッダー ファイルを処理し、その処理の結果を取得して、文字列をその結果でa.h
置き換えるようにプリプロセッサに指示します。#include "a.h"
- を処理している間
a.h
、プリプロセッサはディレクティブを満たし、#include "b.h"
同じメカニズムが適用されます。プリプロセッサはヘッダー ファイルを処理し、その処理の結果を取得し、ディレクティブをその結果b.h
に置き換えます。#include
- を処理するとき
b.h
、ディレクティブ#include "a.h"
はプリプロセッサにa.h
、そのディレクティブを処理して結果に置き換えるように指示します。
- プリプロセッサは再び解析を開始
a.h
し、ディレクティブを再び満たし#include "b.h"
ます。これにより、潜在的に無限の再帰プロセスが設定されます。重要なネスティング レベルに達すると、コンパイラはエラーを報告します。
ただし、インクルード ガードが存在する場合、手順 4 で無限再帰は設定されません。理由を見てみましょう。
- (前と同じ) 解析時
main.cpp
に、プリプロセッサはディレクティブを満たし#include "a.h"
ます。これは、ヘッダー ファイルを処理し、その処理の結果を取得し、文字列をその結果でa.h
置き換えるようにプリプロセッサに指示します。#include "a.h"
- 処理
a.h
中、プリプロセッサは指令を満たし#ifndef A_H
ます。マクロA_H
はまだ定義されていないため、次のテキストを処理し続けます。後続のディレクティブ ( #defines A_H
) は、マクロを定義しA_H
ます。次に、プリプロセッサはディレクティブを満たします#include "b.h"
。プリプロセッサはヘッダー ファイルを処理し、その処理の結果を取得して、ディレクティブをその結果b.h
に置き換えます。#include
- 処理するとき
b.h
、プリプロセッサはディレクティブを満たし#ifndef B_H
ます。マクロB_H
はまだ定義されていないため、次のテキストを処理し続けます。後続のディレクティブ ( #defines B_H
) は、マクロを定義しB_H
ます。次に、ディレクティブ#include "a.h"
はプリプロセッサに処理を指示し、 in のディレクティブを前処理の結果にa.h
置き換えます。#include
b.h
a.h
- コンパイラは前処理を再開し、指令を再び
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 番目の手順のほとんどが、この変更の影響を受けないことは容易にわかります (確認するには、それらを一読してください)。#include "a.h"
ただし、ステップ 4 の最後で別のことが起こります。ディレクティブを空の文字列に置き換えた後、プリプロセッサは の内容、特に の定義のb.h
解析を開始します。残念ながら、包含ガードのおかげで、これまで一度も満たされたことのない言及クラスの定義!b.h
B
B
A
もちろん、以前に宣言されていない型のメンバー変数を宣言することはエラーであり、コンパイラはそれを丁寧に指摘します。
問題を解決するにはどうすればよいですか?
前方宣言が必要です。
実際、 class を定義するためにclassの定義A
は必要ありませんB
。これは、へのポインタA
が type のオブジェクトではなくメンバー変数として宣言されているためA
です。A
ポインタのサイズは固定されているため、コンパイラはclass を適切に定義するためにの正確なレイアウトを知る必要も、そのサイズを計算する必要もありませんB
。したがって、クラスを前方宣言し、コンパイラにその存在を認識させるだけで十分です。A
b.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
は確かにコンパイルされます。いくつかのコメント:
#include
ディレクティブを前方宣言に置き換えることで相互包含を壊すだけでなく、 onb.h
の依存関係を効果的に表現するのに十分でした:全体のコンパイル時間を短縮します。ただし、相互包含を排除した後は、両方に変更する必要があります(後者が必要な場合) 。B
A
main.cpp
#include
a.h
b.h
b.h
#include
a.h
- クラスの前方宣言は
A
、コンパイラがそのクラスへのポインターを宣言する (または不完全な型が許容される他のコンテキストで使用する) のに十分ですが、ポインターの逆参照A
(たとえば、メンバー関数を呼び出すため) やそのサイズの計算は、不完全な型に対する不正なA
操作: 必要な場合は、完全な定義をコンパイラで利用できるようにする必要があります。つまり、それを定義するヘッダー ファイルをインクルードする必要があります。これが、クラス定義とそのメンバー関数の実装が通常、そのクラスのヘッダー ファイルと実装ファイルに分割される理由です (クラステンプレートはこの規則の例外です) #include
。 、安全にできます#include
定義を表示するために必要なすべてのヘッダー。一方、ヘッダー ファイルは、実際にそうする必要がある場合 (たとえば、基本クラスの定義を表示するため) を除き#include
、他のヘッダー ファイルを使用せず、可能な限り/実用的な場合は常に前方宣言を使用します。
2 番目の質問:
複数の定義を防ぐガードを含めないのはなぜですか?
彼らはです。
それらがあなたを保護していないのは、別々の翻訳単位での複数の定義です。これについては、このStackOverflowの Q&Aでも説明されています。
それを見て、インクルードガードを削除し、次の変更されたバージョンsource1.cpp
(またはsource2.cpp
、重要なことは)をコンパイルしてみてください。
//================================================
// source1.cpp
//
// Good luck getting this to compile...
#include "header.h"
#include "header.h"
int main()
{
...
}
コンパイラは、ここで再定義されたことについて文句を言うでしょうf()
。それは明らかです: その定義は 2 回含まれています! ただし、適切な include guards が含まれてsource1.cpp
いる場合、上記は問題なくコンパイルさheader.h
れます。それは予想されます。
それでも、インクルード ガードが存在し、コンパイラがエラー メッセージを表示しなくなる場合でも、リンカsource1.cpp
はとのコンパイルから取得したオブジェクト コードをマージするときに複数の定義が見つかったという事実を主張し、source2.cpp
生成を拒否します。実行可能。
なぜこうなった?
基本的に、プロジェクト内の各.cpp
ファイル (このコンテキストでの専門用語は翻訳単位) は、個別に独立してコンパイルされます。.cpp
ファイルを解析するとき、プリプロセッサはすべての#include
ディレクティブを処理し、遭遇したすべてのマクロ呼び出しを展開します。この純粋なテキスト処理の出力は、オブジェクト コードに変換するためにコンパイラへの入力として提供されます。コンパイラが 1 つの翻訳単位のオブジェクト コードの生成を完了すると、次の翻訳単位に進み、前の翻訳単位の処理中に検出されたすべてのマクロ定義は忘れられます。
n
実際、翻訳単位 (ファイル) を使用してプロジェクトをコンパイルすること.cpp
は、同じプログラム (コンパイラ)n
を毎回異なる入力で実行するようなものです: 同じプログラムの異なる実行は、前のプログラム実行の状態を共有しません。 )。したがって、各翻訳は独立して実行され、1 つの翻訳単位のコンパイル中に検出されたプリプロセッサ シンボルは、他の翻訳単位のコンパイル時には記憶されません (少し考えれば、これが実際には望ましい動作であることが容易にわかるでしょう)。
したがって、インクルード ガードは、1 つの翻訳単位での同じヘッダーの再帰的な相互インクルードや冗長なインクルードを防ぐのに役立ちますが、同じ定義が別の翻訳単位に含まれているかどうかを検出することはできません。
しかし、.cpp
プロジェクトのすべてのファイルのコンパイルから生成されたオブジェクト コードをマージすると、リンカーは同じシンボルが複数回定義されていることを認識します。これは、One Definition Ruleに違反するためです。C++11 標準のパラグラフ 3.2/3:
すべてのプログラムには、そのプログラムで ODR で使用されるすべての非インライン関数または変数の定義が 1 つだけ含まれている必要があります。診断は必要ありません。定義は、プログラム内で明示的に表示されるか、標準またはユーザー定義ライブラリーで見つけることができます。または (適切な場合) 暗黙的に定義されます (12.1、12.4、および 12.8 を参照)。インライン関数は、odr-used であるすべての翻訳単位で定義されます。
したがって、リンカーはエラーを発行し、プログラムの実行可能ファイルの生成を拒否します。
問題を解決するにはどうすればよいですか?
複数の#include
翻訳単位で指定されたヘッダー ファイルに関数定義を保持する場合(ヘッダーが1 つの翻訳単位で指定されている場合でも問題は発生しないことに注意してください)、キーワードを使用する必要があります。#include
inline
それ以外の場合は、関数の宣言header.h
のみを に保持し、その定義 (本体) を1 つの別個の.cpp
ファイルにのみ入れる必要があります (これは従来のアプローチです)。
このinline
キーワードは、通常の関数呼び出し用にスタック フレームを設定するのではなく、呼び出しサイトで関数の本体を直接インライン化するという、コンパイラへの拘束力のない要求を表します。コンパイラは要求を満たす必要はありませんが、inline
キーワードはリンカーに複数のシンボル定義を許容するように指示することに成功しています。C++11 標準のパラグラフ 3.2/5 によると:
クラス型 (条項 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
に関数を配置することです。C++11 標準のパラグラフ 3.5/4:f()
名前のない名前空間、または名前のない名前空間内で直接的または間接的に宣言された名前空間には、内部リンケージがあります。他のすべての名前空間には外部リンケージがあります。上記の内部リンケージが与えられていない名前空間スコープを持つ名前は、それが次の名前である場合、囲んでいる名前空間と同じリンケージを持ちます。
- 変数; また
—関数; また
— 名前付きクラス (条項 9)、またはクラスがリンケージ目的で typedef 名を持つ typedef 宣言で定義された名前のないクラス (7.1.3); また
— 名前付き列挙 (7.2)、または列挙がリンケージ目的で typedef 名を持つ typedef 宣言で定義された名前のない列挙 (7.1.3)。また
— リンケージを持つ列挙に属する列挙子。また
— テンプレート。
上記と同じ理由で、inline
キーワードを優先する必要があります。