🔭

前方宣言のつかいどころ

2025/03/01に公開

2つのヘッダーファイルの場合の循環参照

C++では、以下のように2つのヘッダファイルがお互いをインクルードしようとするとビルドエラーになります。

A.h
#ifndef A_H
#define A_H

#include "B.h"

class A
{
public:
    A() {}
private:
    B* memberB;
};

#endif /*A_H*/
B.h
#ifndef B_H
#define B_H

#include "A.h"
 
class B
{
public:
    B() {}
private:
    A* memberA;
};

#endif /*B_H*/
main.cpp
#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の処理は大体以下の流れになります。

  1. #include "A.h"main.cppA.hppの内容をコピペする
  2. #ifndef A_HのチェックはA_Hがまだ定義されていないため成功する
  3. #define A_HA_Hが定義される
  4. #include "B.h"main.cppB.hppの内容をコピペする
  5. #ifndef B_HのチェックはB_Hがまだ定義されていないため成功する
  6. #define B_HB_Hが定義される
  7. #include "A.h"で再びmain.cppA.hppの内容をコピペする
  8. #ifndef A_HのチェックはA_Hが既に定義されているため失敗し、#endifに移動する
  9. 2回目のA.hの処理が終わって、B.hに戻る
  10. class BBというクラスの定義が始まりますが、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++の記法としてこういう状態を解消できる方法がないか、考えてみることにします。
※「ループになるような設計の是非」はいったん考えないことにして、純粋に文法のことだけ考えてみます

前方宣言

以下のように、どちらかのヘッダーファイルからインクルードを削除して、代わりに前方宣言を追加すれば、一方的な依存関係になり、無限ループを回避できます。

A.h
#ifndef A_H
#define A_H

class B;
 
class A
{
public:
    A() {}
private:
    B* memberB;
};

#endif
B.h
#ifndef B_H
#define B_H

#include "A.h"
 
class B
{
public:
    B() {}
private:
    A* memberA;
};

#endif /*B_H*/
main.cpp
#include "A.h"
#include "B.h"

int main() 
{
    A a;
    B b;
}

名前空間内のクラスの前方宣言

前方宣言で注意しないといけないのは、ネームスペースの中で定義されているクラスの場合は、前方宣言する時も同じネームスペースの中でしないといけないです。そうしないと違うクラスの前方宣言になってしまいます。

A.h

#include "B.h"

namespace space
{
    namespace solver
    {
        class B;
    }
}

class A
{
public:
    A() {}
private:
    B* memberB;
};
B.h
#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とかも前方宣言できます。

まとめ

インクルードの代わりに前方宣言を使うと、コンパイル時間が減るため、前方宣言はできるだけ使った方がいいと言う人もいるかもしれませんが、定義が変わるたびに前方宣言の方も合わせて変えないといけなくてダブルメンテになるため、個人的には本当に使わないといけない時のみ使った方がいいと思います。


|cpp記事一覧へのリンク|

Discussion