クラス間の循環依存関係によるビルドエラーを解決する 質問する

クラス間の循環依存関係によるビルドエラーを解決する 質問する

私は、C++ プロジェクトで、いくつかの不適切な設計決定 (他の誰かが行った :) ) が原因で、複数のコンパイル/リンカー エラーに直面する状況によく遭遇します。このため、異なるヘッダー ファイル内の C++ クラス間の循環依存関係が発生します(同じファイル内でも発生する可能性があります)。しかし、幸いなことに (?)、この問題はそれほど頻繁には発生しないため、次に同じ問題が発生したときのために、この問題の解決策を思い出すことができます。

そこで、将来簡単に思い出せるように、代表的な問題とその解決策を投稿します。もちろん、より良い解決策があれば歓迎します。


  • A.h

    class B;
    class A
    {
        int _val;
        B *_b;
    public:
    
        A(int val)
            :_val(val)
        {
        }
    
        void SetB(B *b)
        {
            _b = b;
            _b->Print(); // COMPILER ERROR: C2027: use of undefined type 'B'
        }
    
        void Print()
        {
            cout<<"Type:A val="<<_val<<endl;
        }
    };
    

  • B.h

    #include "A.h"
    class B
    {
        double _val;
        A* _a;
    public:
    
        B(double val)
            :_val(val)
        {
        }
    
        void SetA(A *a)
        {
            _a = a;
            _a->Print();
        }
    
        void Print()
        {
            cout<<"Type:B val="<<_val<<endl;
        }
    };
    

  • main.cpp

    #include "B.h"
    #include <iostream>
    
    int main(int argc, char* argv[])
    {
        A a(10);
        B b(3.14);
        a.Print();
        a.SetB(&b);
        b.Print();
        b.SetA(&a);
        return 0;
    }
    

ベストアンサー1

これについて考える方法は、「コンパイラのように考える」ことです。

コンパイラを書いているところを想像してください。そして、次のようなコードを見たとします。

// file: A.h
class A {
  B _b;
};

// file: B.h
class B {
  A _a;
};

// file main.cc
#include "A.h"
#include "B.h"
int main(...) {
  A a;
}

.ccファイルをコンパイルする場合(.h ではなく .cc がコンパイル単位であることに注意してください)オブジェクトにスペースを割り当てる必要があります。では、スペースはどれくらいでしょうか? を格納するのに十分な大きさですか? では、のサイズはどれくらいでしょうか? を格納するのに十分な大きさです! おっと。ABBA

明らかに循環参照なので、これを解消する必要があります。

代わりに、コンパイラが事前に知っている限りのスペースを予約できるようにすることで、この制約を破ることができます。たとえば、ポインタと参照は常に 32 ビットまたは 64 ビット (アーキテクチャによって異なります) であるため、(どちらかを) ポインタまたは参照に置き換えると、状況は良くなります。次のように置き換えてみましょうA

// file: A.h
class A {
  // both these are fine, so are various const versions of the same.
  B& _b_ref;
  B* _b_ptr;
};

今は状況は良くなりました。多少は。main()それでもこう言います。

// file: main.cc
#include "A.h"  // <-- Houston, we have a problem

#includeは、あらゆる範囲と目的において (プリプロセッサを取り除けば) ファイルを.ccにコピーするだけです。つまり、実際には.cc は次のようになります。

// file: partially_pre_processed_main.cc
class A {
  B& _b_ref;
  B* _b_ptr;
};
#include "B.h"
int main (...) {
  A a;
}

コンパイラがこれを処理できない理由はおわかりでしょう。コンパイラには何が何だかわかりませんB。これまでそのシンボルを見たことがないからです。

そこでコンパイラに について伝えましょうB。これは と呼ばれます。前方宣言、そしてさらに詳しくはこの答え

// main.cc
class B;
#include "A.h"
#include "B.h"
int main (...) {
  A a;
}

これは動作します。素晴らしいものではありません。しかし、この時点で、循環参照の問題と、たとえ修正が不十分であったとしても、それを「修正」するために私たちが行ったことを理解しているはずです。

この修正がうまくいかない理由は、次の人がそれを使用する前に#include "A.h"宣言する必要がありB、ひどい#includeエラーが発生するからです。そこで、宣言をAh自体に移動しましょう。

// file: A.h
class B;
class A {
  B* _b; // or any of the other variants.
};

そして、 Bhでは、この時点で、直接実行できます#include "A.h"

// file: B.h
#include "A.h"
class B {
  // note that this is cool because the compiler knows by this time
  // how much space A will need.
  A _a; 
}

ありがとう。

おすすめ記事