web-dev-qa-db-ja.com

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

私は、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;
    }
    
310
Autodidact

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

あなたがコンパイラを書いていると想像してください。そして、このようなコードが表示されます。

// 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ファイルをコンパイルするとき(。cc。hはコンパイルの単位ではありません)、オブジェクトAにスペースを割り当てる必要があります。それで、それでは、どのくらいのスペースですか? Bを保存するのに十分です!それではBのサイズは? Aを保存するのに十分です!おっとっと。

明らかに、破らなければならない循環参照。

代わりに、コンパイラが事前に知っている限りのスペースを確保できるようにすることで、これを破ることができます-例えば、ポインターと参照は、常に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についてコンパイラーに伝えましょう。これは 前方宣言 として知られており、 this answer でさらに説明されています。

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

これはworksgreatではありません。しかし、この時点で、循環参照の問題と、それが「修正」するために何をしたかを理解する必要がありますが、修正は悪いものです。

この修正が悪い理由は、#include "A.h"の次の人が使用する前にBを宣言する必要があり、ひどい#includeエラーを受け取るためです。それでは、宣言をA.h自体に移動しましょう。

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

そしてB.hでは、この時点で、直接#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; 
}

HTH。

252
Roosh

メソッド定義をヘッダーファイルから削除し、クラスにメソッド宣言と変数宣言/定義のみを含めるようにすれば、コンパイルエラーを回避できます。メソッド定義は.cppファイルに配置する必要があります(ベストプラクティスガイドラインに記載されているとおりです)。

次のソリューションの欠点は(ヘッダーファイルにメソッドを配置してメソッドをインライン化した場合)、メソッドがコンパイラーによってインライン化されなくなり、inlineキーワードを使用しようとするとリンカーエラーが発生することです。

//A.h
#ifndef A_H
#define A_H
class B;
class A
{
    int _val;
    B* _b;
public:

    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

//B.h
#ifndef B_H
#define B_H
class A;
class B
{
    double _val;
    A* _a;
public:

    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

//A.cpp
#include "A.h"
#include "B.h"

#include <iostream>

using namespace std;

A::A(int val)
:_val(val)
{
}

void A::SetB(B *b)
{
    _b = b;
    cout<<"Inside SetB()"<<endl;
    _b->Print();
}

void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

//B.cpp
#include "B.h"
#include "A.h"
#include <iostream>

using namespace std;

B::B(double val)
:_val(val)
{
}

void B::SetA(A *a)
{
    _a = a;
    cout<<"Inside SetA()"<<endl;
    _a->Print();
}

void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

//main.cpp
#include "A.h"
#include "B.h"

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;
}
96
Autodidact

覚えておくべきこと:

  • class Aclass Bのオブジェクトをメンバーとして持つ場合、またはその逆の場合、これは機能しません。
  • 前方宣言は進むべき道です。
  • 宣言の順序が重要です(そのため、定義を削除しています)。
    • 両方のクラスが他のクラスの関数を呼び出す場合、定義を外に移動する必要があります。

FAQを読む:

17
dirkgently

私はこれに答えるのが遅れていますが、非常に支持された答えを持つ人気のある質問であるにもかかわらず、これまでのところ合理的な答えはありません....

ベストプラクティス:前方宣言ヘッダー

標準ライブラリの<iosfwd>ヘッダーで示されているように、他のユーザーに前方宣言を提供する適切な方法は、forward宣言ヘッダーを使用することです。例えば:

a.fwd.h:

#pragma once
class A;

a.h:

#pragma once
#include "a.fwd.h"
#include "b.fwd.h"

class A
{
  public:
    void f(B*);
};

b.fwd.h:

#pragma once
class B;

b.h:

#pragma once
#include "b.fwd.h"
#include "a.fwd.h"

class B
{
  public:
    void f(A*);
};

AおよびBライブラリのメンテナーは、それぞれ、フォワード宣言ヘッダーをヘッダーおよび実装ファイルと同期させる責任があります。たとえば、「B」のメンテナーが来て、コードを書き直した場合...

b.fwd.h:

template <typename T> class Basic_B;
typedef Basic_B<char> B;

b.h:

template <typename T>
class Basic_B
{
    ...class definition...
};
typedef Basic_B<char> B;

...「A」のコードの再コンパイルは、含まれるb.fwd.hへの変更によってトリガーされ、正常に完了するはずです。


悪いが一般的な慣行:他のライブラリで前方宣言する

たとえば、上記で説明したように前方宣言ヘッダーを使用する代わりに、a.hまたはa.ccのコードで、代わりにclass B;自体を前方宣言します。

  • a.hまたはa.ccに後でb.hが含まれていた場合:
    • Bの競合する宣言/定義に到達すると、Aのコンパイルはエラーで終了します(つまり、上記のBへの変更により、透過的に動作するのではなく、前方宣言を悪用する他のクライアントがAを破損しました)。
  • そうでない場合(最終的にAがb.hを含まなかった場合-Aがポインターや参照によってBを格納/渡す場合のみ可能)
    • #include分析と変更されたファイルのタイムスタンプに依存するビルドツールは、Bへの変更後にA(およびさらに依存するコード)を再構築せず、リンク時または実行時にエラーを引き起こします。 BがランタイムロードされたDLLとして配布される場合、「A」のコードは実行時に異なるマングル記号を見つけることができない場合があります。

Aのコードに古いBのテンプレート特化/「特性」がある場合、それらは有効になりません。

16
Tony Delroy

すべてのinlinesをクラス定義の後に移動し、ヘッダーファイルでinlinesの直前に他のクラスの#includeを配置することで、この種の問題を解決しました。このようにして、インラインが解析される前にすべての定義とインラインが設定されていることを確認します。

このようにすることで、両方の(または複数の)ヘッダーファイルにインラインの束を保持することができます。ただし、ガードを含めるにする必要があります。

このような

// File: A.h
#ifndef __A_H__
#define __A_H__
class B;
class A
{
    int _val;
    B *_b;
public:
    A(int val);
    void SetB(B *b);
    void Print();
};

// Including class B for inline usage here 
#include "B.h"

inline A::A(int val) : _val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif /* __A_H__ */

...そしてB.hで同じことをする

11
epatel

これについては一度投稿しました: C++での循環依存関係の解決

基本的な手法は、インターフェースを使用してクラスを分離することです。あなたの場合:

//Printer.h
class Printer {
public:
    virtual Print() = 0;
}

//A.h
#include "Printer.h"
class A: public Printer
{
    int _val;
    Printer *_b;
public:

    A(int val)
        :_val(val)
    {
    }

    void SetB(Printer *b)
    {
        _b = b;
        _b->Print();
    }

    void Print()
    {
        cout<<"Type:A val="<<_val<<endl;
    }
};

//B.h
#include "Printer.h"
class B: public Printer
{
    double _val;
    Printer* _a;
public:

    B(double val)
        :_val(val)
    {
    }

    void SetA(Printer *a)
    {
        _a = a;
        _a->Print();
    }

    void Print()
    {
        cout<<"Type:B val="<<_val<<endl;
    }
};

//main.cpp
#include <iostream>
#include "A.h"
#include "B.h"

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;
}
6
Eduard Wirch

テンプレートのソリューションは次のとおりです。 テンプレートを使用して循環依存関係を処理する方法

この問題を解決する手がかりは、定義(実装)を提供する前に両方のクラスを宣言することです。宣言と定義を別々のファイルに分割することはできませんが、それらを別々のファイルにあるかのように構造化できます。

3
Tatyana

ウィキペディアに掲載されている簡単な例は私にとってはうまくいきました。 (詳細な説明は http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B で読むことができます)

ファイル '' 'a.h' '':

#ifndef A_H
#define A_H

class B;    //forward declaration

class A {
public:
    B* b;
};
#endif //A_H

ファイル '' 'b.h' '':

#ifndef B_H
#define B_H

class A;    //forward declaration

class B {
public:
    A* a;
};
#endif //B_H

ファイル '' 'main.cpp' '':

#include "a.h"
#include "b.h"

int main() {
    A a;
    B b;
    a.b = &b;
    b.a = &a;
}
2
madx

残念ながら、gezaからの回答にコメントすることはできません。

彼は「宣言を別のヘッダーに転送する」と言っているだけではありません。 「遅延した依存関係」を可能にするために、クラス定義ヘッダーとインライン関数定義を異なるヘッダーファイルにスピルする必要があると彼は言います。

しかし、彼のイラストはあまり良くありません。両方のクラス(AおよびB)は、互いに不完全な型(ポインターフィールド/パラメーター)のみを必要とするためです。

それを理解するために、クラスAにはB *ではなくタイプBのフィールドがあると想像してください。さらに、クラスAとBは、他のタイプのパラメーターを使用してインライン関数を定義したいと考えています。

この単純なコードは機能しません。

// A.h
#pragme once
#include "B.h"

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}

// B.h
#pragme once
class A;

class B{
  A* b;
  inline void Do(A a);
}

#include "A.h"

inline void B::Do(A a){
  //do something with A
}

//main.cpp
#include "A.h"
#include "B.h"

次のコードになります。

//main.cpp
//#include "A.h"

class A;

class B{
  A* b;
  inline void Do(A a);
}

inline void B::Do(A a){
  //do something with A
}

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}
//#include "B.h"

B :: Doは後で定義される完全なタイプのAを必要とするため、このコードはコンパイルされません。

ソースコードがコンパイルされるようにするには、次のようにします。

//main.cpp
class A;

class B{
  A* b;
  inline void Do(A a);
}

class A{
  B b;
  inline void Do(B b);
}

inline void B::Do(A a){
  //do something with A
}

inline void A::Do(B b){
  //do something with B
}

これは、インライン関数を定義する必要がある各クラスのこれらの2つのヘッダーファイルで正確に可能です。唯一の問題は、循環クラスに「パブリックヘッダー」を含めることができないことです。

この問題を解決するには、プリプロセッサ拡張機能をお勧めします:#pragma process_pending_includes

このディレクティブは、現在のファイルの処理を延期し、保留中のインクルードをすべて完了する必要があります。

0
Bernd Baumanns

場合によっては、defineクラスAのヘッダーファイル内のクラスBのメソッドまたはコンストラクターを使用して、定義に関連する循環依存関係を解決することができます。このようにして、ヘッダーのみのライブラリを実装する場合など、.ccファイルに定義を配置する必要がなくなります。

// file: a.h
#include "b.h"
struct A {
  A(const B& b) : _b(b) { }
  B get() { return _b; }
  B _b;
};

// note that the get method of class B is defined in a.h
A B::get() {
  return A(*this);
}

// file: b.h
class A;
struct B {
  // here the get method is only declared
  A get();
};

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

0
Jonas

残念ながら、これまでの答えにはすべて詳細が欠けています。正しい解決策は少し面倒ですが、これが適切にそれを行う唯一の方法です。また、簡単に拡張でき、より複雑な依存関係も処理します。

これを行う方法は次のとおりです。すべての詳細と使いやすさを正確に保持します。

  • 解決策は当初の意図とまったく同じです
  • インライン関数はまだインライン
  • AおよびBのユーザーは、A.hおよびB.hを任意の順序で含めることができます

2つのファイル、A_def.h、B_def.hを作成します。これらには、ABの定義のみが含まれます。

// A_def.h
#ifndef A_DEF_H
#define A_DEF_H

class B;
class A
{
    int _val;
    B *_b;

public:
    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

// B_def.h
#ifndef B_DEF_H
#define B_DEF_H

class A;
class B
{
    double _val;
    A* _a;

public:
    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

そして、A.hとB.hには以下が含まれます。

// A.h
#ifndef A_H
#define A_H

#include "A_def.h"
#include "B_def.h"

inline A::A(int val) :_val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif

// B.h
#ifndef B_H
#define B_H

#include "A_def.h"
#include "B_def.h"

inline B::B(double val) :_val(val)
{
}

inline void B::SetA(A *a)
{
    _a = a;
    _a->Print();
}

inline void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

#endif

A_def.hおよびB_def.hは「プライベート」ヘッダーであり、AおよびBのユーザーは使用しないでください。公開ヘッダーはA.hおよびB.hです。

0
geza