前方宣言のつかいどころ
2つのヘッダーファイルの場合の循環参照
C++では、以下のように2つのヘッダファイルがお互いをインクルードしようとするとビルドエラーになります。
#ifndef A_H
#define A_H
#include "B.h"
class A
{
public:
A() {}
private:
B* memberB;
};
#endif /*A_H*/
#ifndef B_H
#define B_H
#include "A.h"
class B
{
public:
B() {}
private:
A* memberA;
};
#endif /*B_H*/
#include "A.h"
#include "B.h"
int main()
{
A a;
B b;
}
でもビルドエラーを見てみると、以下のようにA
が定義されていないエラーになります。これはなんででしょうか?
./B.h:11:2: error: unknown type name ‘A’
8 | A a;
もちろんコンパイラーによると思いますが、main.cpp
の処理は大体以下の流れになります。
-
#include "A.h"
でmain.cpp
にA.hpp
の内容をコピペする -
#ifndef A_H
のチェックはA_H
がまだ定義されていないため成功する -
#define A_H
でA_H
が定義される -
#include "B.h"
でmain.cpp
にB.hpp
の内容をコピペする -
#ifndef B_H
のチェックはB_H
がまだ定義されていないため成功する -
#define B_H
でB_H
が定義される -
#include "A.h"
で再びmain.cpp
にA.hpp
の内容をコピペする -
#ifndef A_H
のチェックはA_H
が既に定義されているため失敗し、#endif
に移動する - 2回目の
A.h
の処理が終わって、B.h
に戻る -
class B
でB
というクラスの定義が始まりますが、A
がまだ定義されていないせいで、メンバー変数のA* memberA;
の宣言でエラーになる
この時点でmain.cpp
は以下の内容になっていると思います。
// 1回目の #include "A.h" の始まり
#ifndef A_H
#define A_H
// 1回目の #include "B.h" の始まり
#ifndef B_H
#define B_H
// 2回目の #include "A.h" の始まり
#ifndef A_H
// 中身はスキップされるため省略
#endif /*A_H*/
// 2回目の #include "A.h" の終わり
class B
{
public:
B() {}
private:
A* memberA;
};
#endif /*B_H*/
// 1回目の #include "B.h" の終わり
class A
{
public:
A() {}
private:
B* memberB;
};
#endif /*A_H*/
// 1回目の #include "A.h" の終わり
int main()
{
A a;
B b;
}
でもエラーがこうなっているだけで、エラーになっていなくても、そもそも2つのヘッダーファイルがお互いをインクルードすると、無限ループになるため、まずいです。
c++の記法としてこういう状態を解消できる方法がないか、考えてみることにします。
※「ループになるような設計の是非」はいったん考えないことにして、純粋に文法のことだけ考えてみます
前方宣言
以下のように、どちらかのヘッダーファイルからインクルードを削除して、代わりに前方宣言を追加すれば、一方的な依存関係になり、無限ループを回避できます。
#ifndef A_H
#define A_H
class B;
class A
{
public:
A() {}
private:
B* memberB;
};
#endif
#ifndef B_H
#define B_H
#include "A.h"
class B
{
public:
B() {}
private:
A* memberA;
};
#endif /*B_H*/
#include "A.h"
#include "B.h"
int main()
{
A a;
B b;
}
名前空間内のクラスの前方宣言
前方宣言で注意しないといけないのは、ネームスペースの中で定義されているクラスの場合は、前方宣言する時も同じネームスペースの中でしないといけないです。そうしないと違うクラスの前方宣言になってしまいます。
#include "B.h"
namespace space
{
namespace solver
{
class B;
}
}
class A
{
public:
A() {}
private:
B* memberB;
};
#ifndef B_H
#define B_H
namespace space
{
namespace solver
{
class B
{
public:
B() {}
private:
A* memberA;
};
}
}
#endif
1つのヘッダーファイルの場合の循環参照
以下のように同じファイルでお互いに依存しているクラスを定義する時も、前方宣言が必要になります。
class B;
class A
{
public:
A() {}
private:
B* memberB;
};
class B
{
public:
B() {}
private:
A* memberA;
};
前方宣言の制限
でも前方宣言されたものは、ポインタ型か参照型としてしか使えないです。例えば、以下のようにmemberB
を実体で宣言すると、不完全な型というエラーになります。
class B;
class A
{
public:
A() {}
private:
B memberB; // ポインタ/参照ではなく実体
};
class B
{
public:
B() {}
private:
A* memberA;
};
ポインタ型か参照型でも、以下のように実際にメンバー関数かメンバー変数にアクセスしようとすると、同じく不完全な型というエラーになります。
class B;
class A
{
public:
A()
{
memberB->function(); // メンバー関数を呼びだす
}
private:
B* memberB;
};
class B
{
public:
B() {}
void function() {}
private:
A* memberA;
};
構造体と関数とenum
とかも前方宣言できます。
まとめ
インクルードの代わりに前方宣言を使うと、コンパイル時間が減るため、前方宣言はできるだけ使った方がいいと言う人もいるかもしれませんが、定義が変わるたびに前方宣言の方も合わせて変えないといけなくてダブルメンテになるため、個人的には本当に使わないといけない時のみ使った方がいいと思います。
Discussion