エラーが指している行を、何度見ても、おかしなところがありません。A* partner = nullptr;。型の名前も、変数名も、正しい。なのに、C2143: 構文エラー: ';' が '*' の前にありませんと言われます。しかも、そのあとに、まったく別の行で、関連のなさそうなエラーが続きます。
こういうとき、エラーが指す行を直そうとしても、直りません。原因は、別のファイルの#includeの並びにあるからです。この記事では、AとBが、お互いのヘッダーを読む「循環インクルード」を、わざと作って、エラー文の出方と、直し方を、MSVC(19.44、x64)の出力で確かめました。
循環インクルードとは?
循環インクルードとは、
・A.hがB.hを読み込み、B.hもA.hを読み込む、というように、ヘッダーが輪になって、お互いを読み合っている状態
です。ゲームでいえば、「PlayerがEnemyを指して、EnemyもPlayerを指す」ような設計のときに、起きやすくなります。ポインタで相手を持つだけなら、型の名前が分かれば十分なのに、つい、#includeで済ませてしまうのが、原因です。
実験①:A.hとB.hが、お互いを読む
2つのクラスが、ポインタで、お互いを持つようにしました。ヘッダーには、インクルードガード(#pragma once)も、きちんと付けています。
▼A.h
#pragma once
#include "B.h"
class A
{
public:
B* partner = nullptr;
int hp = 100;
};▼B.h
#pragma once
#include "A.h"
class B
{
public:
A* partner = nullptr;
int mp = 50;
};▼main.cpp(A.hを読む)
#include <cstdio>
#include "A.h"
int main()
{
A a;
B b;
a.partner = &b;
b.partner = &a;
std::printf("a.hp = %d, b.mp = %d\n", a.hp, b.mp);
return 0;
}main.cpp
C:\blogtest\cinc\c1\B.h(7): error C2143: 構文エラー: ';' が '*' の前にありません。
C:\blogtest\cinc\c1\B.h(7): error C4430: 型指定子がありません - int と仮定しました。メモ: C++ は int を既定値としてサポートしていません
C:\blogtest\cinc\c1\B.h(7): error C2238: ';' の前に無効なトークンがあります。
main.cpp(9): error C2039: 'partner': 'B' のメンバーではありません
C:\blogtest\cinc\c1\B.h(4): note: 'B' の宣言を確認してくださいエラーは、B.hの7行目に出ました。A* partner = nullptr;の行です。しかも、「;が*の前にありません」「型指定子がありません(intと仮定しました)」と、3つのエラーが、同じ行に並びます。そのあと、main.cppの9行目では、「partnerはBのメンバーではありません」と、別の場所でも、別の言い方で、エラーが出ました。
どれも、直接の原因ではありません。1つ目のエラーで、Bの中身が、正しく作られなかったために、あとのエラーは、その連鎖で出ています。
【重要】エラーが出る場所は、読み込む順番で変わる
main.cppが、A.hではなく、B.hを先に読むように変えて、もう一度ビルドしました。
main.cpp
C:\blogtest\cinc\c1b\A.h(7): error C2143: 構文エラー: ';' が '*' の前にありません。
C:\blogtest\cinc\c1b\A.h(7): error C4430: 型指定子がありません - int と仮定しました。メモ: C++ は int を既定値としてサポートしていません
C:\blogtest\cinc\c1b\A.h(7): error C2238: ';' の前に無効なトークンがあります。
main.cpp(8): error C2039: 'partner': 'A' のメンバーではありません
C:\blogtest\cinc\c1b\A.h(4): note: 'A' の宣言を確認してください今度は、A.hの7行目が、同じ形のエラーになりました。ヘッダーの中身は、1文字も変えていません。変えたのは、main.cppの#includeの順番だけです。つまり、エラーが指す行は、「最初に読まれた側のヘッダー」の都合で決まっていて、そこが悪いわけではないのです。
なぜ、こうなる?
#includeは、ヘッダーの中身を、その位置に、そのまま貼り付ける処理です。main.cppがA.hを読んだときの展開は、次の順になります。
A.hの読み込みが始まる。インクルードガードが働いて、「A.hは読み込み済み」になるA.hの先頭の#include "B.h"で、B.hの読み込みが始まるB.hの先頭の#include "A.h"は、「読み込み済み」なので、何も貼り付けられないB.hのclass Bに進む。この時点では、Aというクラスは、まだ登場していない。だから、A* partnerのAが、型として認識されず、構文エラーになる
インクルードガードは、無限ループを防いでくれますが、Aが先に出てくるように、順番を整えてはくれません。
インクルードガードが無いと、どうなる?
ついでに、#pragma onceを外したら、どうなるかも試しました。
main.cpp
C:\blogtest\cinc\c1c\B.h(1): fatal error C1014: インクルード ファイルが多すぎます: 深さ = 1024「インクルード ファイルが多すぎます: 深さ = 1024」というエラー(C1014)で、コンパイルが止まりました。A.hとB.hが、お互いを延々と読み込み続けて、上限の深さに達したのです。ガードを付けると、この無限ループは止まりますが、代わりに、上の順番の問題が出る、ということです。
直し方:前方宣言を使う
前方宣言は、「Bというクラスがある」とだけ、先に伝える書き方です(class B;)。ポインタで持つだけなら、クラスの中身が分からなくても、型の名前さえ分かれば足ります。#includeを、前方宣言に置き換えました。
▼A.h(前方宣言に変えた)
#pragma once
class B; // 前方宣言(Bという名前のクラスがある、とだけ伝える)
class A
{
public:
B* partner = nullptr;
int hp = 100;
};▼B.h(前方宣言に変えた)
#pragma once
class A; // 前方宣言
class B
{
public:
A* partner = nullptr;
int mp = 50;
};main.cppa.hp = 100, b.mp = 50エラーが消えて、実行もできました。ヘッダーどうしが、お互いを読まなくなったので、輪が切れたのです。しかも、前方宣言は、#includeの順番に、左右されません。
前方宣言で直せない場合
前方宣言は、万能ではありません。クラスの中身(大きさやメンバ)が必要になるコードでは、使えません。実際に、2つの場合を試しました。
①メンバとして、実体を持つ
Aが、ポインタではなく、Bの実体を、メンバに持った場合です。
▼A.h(Bの実体をメンバにした)
#pragma once
class B;
class A
{
public:
B partner; // ポインタではなく、実体をメンバに持つ
int hp = 100;
};main.cpp
C:\blogtest\cinc\c3\A.h(8): error C2079: 'A::partner' が 未定義の class 'B' で使用しています。「未定義のclass Bで使用しています」(C2079)というエラーになりました。実体を持つには、Bの大きさを知る必要があるからです。この場合は、B.hを#includeする必要があります。ただし、お互いが実体を持ち合う設計は、そもそも成立しません(AがBを含み、BがAを含むと、大きさが決まらない)。片方をポインタか参照にします。
②ヘッダーの中で、相手のメンバを使う
メンバ関数の中身を、ヘッダーに書いて、相手のメンバ(mp)を使う場合です。
▼A.h(partner->mpを使う関数を、ヘッダーに書いた)
#pragma once
class B;
class A
{
public:
B* partner = nullptr;
int hp = 100;
int partnerMp() const { return partner->mp; } // Bの中身(mp)を使う
};main.cpp
C:\blogtest\cinc\c4\A.h(11): error C2027: 認識できない型 'B' が使われています。
C:\blogtest\cinc\c4\A.h(3): note: 'B' の宣言を確認してください「認識できない型Bが使われています」(C2027)になりました。partner->mpと書くには、Bの中身を知っている必要があります。直し方は、関数の中身を、.cppに移して、そこでB.hを読むことです。
▼A.h(宣言だけにした)
#pragma once
class B;
class A
{
public:
B* partner = nullptr;
int hp = 100;
int partnerMp() const; // 宣言だけ
};▼A.cpp(ここでB.hを読む)
#include "A.h"
#include "B.h" // Bの中身が必要なのは、ここだけ
int A::partnerMp() const
{
return partner->mp;
}main.cpp
A.cpp
コードを生成中...a.hp = 100, b.mp = 50, a.partnerMp() = 50ヘッダーは前方宣言のままで、A.cppだけが、B.hを読みます。ヘッダーどうしの輪は、切れたままです。
前方宣言が使える場合と、使えない場合の早見表
| ヘッダーに書くもの | 前方宣言で足りる? | 今回の結果 |
|---|---|---|
B*(ポインタ)、B&(参照)のメンバ | 足りる | ビルドも実行も成功 |
Bの実体(値)のメンバ | 足りない(B.hが必要) | C2079(未定義のclassで使用) |
相手のメンバ(partner->mp)を使う関数の中身 | 足りない(B.hが必要) | C2027(認識できない型) |
循環インクルードの、よくある失敗例と対処法
①エラーが指す行を、必死に直す
実験のとおり、指されているのは、「たまたま最初に失敗した行」です。その行が正しく見えるときは、その行で使っている型が、先に宣言されているかを、疑います。
②#includeを、並べ替えてごまかす
並べ替えで、エラーが消えることもあります。ただし、A.hとB.hの輪は、残っています。ヘッダーを足すたびに、同じ問題が、別の場所で再発します。輪そのものを、切ります。
③とりあえず、ヘッダーに#includeを足す
ヘッダーに#includeを足すと、そのヘッダーを読むすべてのファイルが、その影響を受けます。ポインタで足りるなら、ヘッダーでは前方宣言、#includeは、.cppの中だけにしておくのが、基本です。ビルド時間の短縮にも、つながります。
注意点
- 循環インクルードのエラーは、エラーが指す行が、原因の行とは限らない
- エラーが出る場所は、
#includeの読み込み順で変わる(今回は、B.hの7行目と、A.hの7行目で入れ替わった) - インクルードガードは、無限ループは防ぐが、順番の問題は解決しない。ガードが無いと、C1014(深さ=1024)になった
- 前方宣言(
class B;)で足りるのは、ポインタや参照で持つとき - 実体のメンバ(C2079)や、相手のメンバの使用(C2027)には、前方宣言は使えない。後者は、関数の中身を
.cppに移す - 今回の検証は、MSVC 19.44(Visual Studio 2022付属、x64、
/std:c++17)で行った。エラー番号や文面は、版で変わりうる
この記事の要点
A.hとB.hがお互いを読むと、先に読まれたほうのヘッダーで、型が見つからないエラーが出る- エラーの行は、読み込み順で入れ替わる。指された行ではなく、
#includeの輪を疑う - 直し方は、ヘッダーでは前方宣言、
#includeは.cppの中だけにして、輪を切ること - 前方宣言で足りるのは、ポインタと参照。実体のメンバや、相手の中身を使う処理は、.cppへ
エラーの行が正しく見えるのは、見た目の不思議ではなく、「読まれる順番の問題」という、はっきりした理由があるからです。輪を切って、ヘッダーを軽くしておけば、新しいクラスを足したときにも、同じ種類のエラーに振り回されにくくなります。

