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


353

異なるヘッダーファイル内の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;
    }
    

23
Visual Studioを使用する場合、/ showIncludesフラグはこの種の問題のデバッグに役立ちます。
wip

回答:


288

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

コンパイラを書いているとしましょう。そして、あなたはこのようなコードを見ます。

// 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がコンパイルの単位であることを忘れないでください)、objectにスペースを割り当てる必要があります。それで、それでは、どのくらいのスペースが必要ですか?保存するのに十分!その時のサイズは?保存するのに十分!おっとっと。ABBA

明らかに、破壊する必要のある循環参照。

コンパイラが前もって知っている限り多くのスペースを代わりに予約することを許可することでそれを壊すことができます-たとえば、ポインタと参照は常に32または64ビット(アーキテクチャに応じて)になるため、(いずれか1つ)をポインタまたは参照、物事は素晴らしいでしょう。次のように置き換えるとしましょう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; 
}

HTH。


20
「Bについてコンパイラに伝えるには、」Bの前方宣言として知られている
ピーターAjtai

8
ああ、神様!参照が占有スペースの観点から知られているという事実を完全に逃しました。ついに、きちんとデザインできるようになりました!
ケルログ

47
しかし、それでもBでは関数を使用できません(質問_b-> Printt()のように)
rank1

3
これは私が抱えている問題です。ヘッダーファイルを完全に書き換えずに、関数を前方宣言でどのように組み込むのですか?
サイダン


101

ヘッダーファイルからメソッド定義を削除し、クラスにメソッド宣言と変数宣言/定義のみを含めれば、コンパイルエラーを回避できます。メソッド定義は.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;
}

ありがとう。これは問題を簡単に解決しました。循環インクルードを.cppファイルに移動しただけです。
Lenar Hoyt 2014

3
テンプレートメソッドがある場合はどうなりますか?テンプレートを手動でインスタンス化しない限り、実際にCPPファイルに移動することはできません。
Malcolm

常に「Ah」と「Bh」を一緒に含めます。「Ah」を「Bh」に含め、「Bh」だけを「A.cpp」と「B.cpp」の両方に含めませんか?
グセフスラバ2018

28

私はこれに答えるのが遅いですが、非常に賛成の回答がある人気のある質問ですが...

ベストプラクティス:宣言ヘッダーを転送する

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

a.fwd.h:

#pragma once
class A;

ああ:

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

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

b.fwd.h:

#pragma once
class B;

bh:

#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;

bh:

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

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


貧弱だが一般的な慣行:他のライブラリの内容を前方宣言する

言う-上記で説明したように転送宣言ヘッダーを使用する代わりに、コードを記述するa.hか、a.cc代わりにclass B;それ自体を転送宣言します。

  • 後で含めるa.ha.cc含めた場合b.h
    • Aのコンパイルは、宣言/定義の競合が発生すると、エラーで終了しますB(つまり、上記のBへの変更により、透過的に動作する代わりに、Aと他のクライアントが前方宣言を乱用しました)。
  • それ以外の場合(Aが最終的に含まなかった場合-Aがb.hポインターまたは参照、あるいはその両方によってBsを単に格納/渡す場合に可能)
    • #include分析と変更されたファイルのタイムスタンプに依存するビルドツールはA、Bへの変更後に再ビルド(およびそのさらに依存するコード)を行わず、リンク時または実行時にエラーが発生します。BがランタイムロードされたDLLとして配布されている場合、「A」のコードは実行時に異なるマングルされたシンボルを見つけられない可能性があります。

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


2
これは、フォワード宣言を処理するための本当にクリーンな方法です。唯一の「欠点」は、追加ファイルにあります。常に同期していることを確認するためa.fwd.ha.h、常にに含めると思います。これらのクラスが使用されているサンプルコードがありません。a.hそしてb.h、彼らは単独では機能しませんので、含まれる両方が必要になります: `` `//main.cppの#include「ああ」の#include(メインint型「BH」は){...}` ``またはそれらの1最初の質問のように、他の質問に完全に含める必要があります。b.h含まれている場所a.hmain.cpp含まれている場所b.h
ファーウェイ2017

2
@Farwayすべての点で正しい。私はわざわざ表示しませんでしたmain.cppが、コメントに何が含まれるべきかを文書化してくれてうれしいです。乾杯
トニー・デルロイ

1
良い点と悪い点がある理由とそうでない理由についての詳細な説明でより良い答えの1つ
フランシス・キュグラー

1
@RezaHajianpour:循環宣言であるかどうかに関係なく、前方宣言が必要なすべてのクラスに前方宣言ヘッダーがあることは理にかなっています。つまり、1)実際の宣言を含めるとコストがかかる(または後で予想される)コストがかかる(たとえば、変換ユニットが必要としないヘッダーがたくさん含まれている)2)クライアントコードがオブジェクトへのポインタまたは参照を利用できる可能性があります。 <iosfwd>は古典的な例です。多くの場所から参照されるいくつかのストリームオブジェクトが存在する可能性が<iostream>あり、多くを含める必要があります。
トニーDelroy

1
@RezaHajianpour:あなたは正しい考えを持っていると思いますが、ステートメントには用語の問題があります。「型を宣言する必要があるだけです」が正しいでしょう。タイプがされていると宣言前方宣言が見られた手段と、それはだ定義された完全な定義が解析されてきた(そしてそのためにあなたは一度より多く必要とする#include複数可)。
トニー・デルロイ

20

覚えておくべきこと:

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

FAQを読む:


1
提供したリンクが機能しなくなった場合、参照する新しいリンクを知っていますか?
Ramya Rao

11

私はかつてこの種の問題を、クラス定義の後にすべてのインラインを移動#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


どうして?私はそれがトリッキーな問題へのエレガントな解決策だと思います...インラインが欲しいとき。インラインが必要ない場合は、最初から書かれたようなコードを書くべきではありませんでした...
epatel

ユーザーがB.h最初に含めるとどうなりますか?
Fooz氏、2014年

3
ヘッダーガードが予約済みの識別子を使用していることに注意してください。二重のアンダースコアが隣接するものはすべて予約されています。
Lars Viklund、2015

6

私はこれについて一度投稿しました: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;
}

2
インターフェースを使用すると、virtual実行時のパフォーマンスに影響することに注意してください。
cemper93 2016年

4

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

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


2

ウィキペディアで紹介されている簡単な例は私のために働いた。(完全な説明は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;
}

1

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

これを行う方法を以下に示します。すべての詳細と使いやすさを正確に保持しています。

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

A_def.h、B_def.hの2つのファイルを作成します。これらには、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

そして、AhとBhはこれを含みます:

// 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それらを使用するべきではありません。パブリックヘッダーはAhおよびBhです。


1
これには、Tony Delroyのソリューションに勝る利点がありますか?どちらも「ヘルパー」ヘッダーに基づいていますが、トニーのヘッダーは小さく(前方宣言が含まれているだけ)、同じように機能しているように見えます(少なくとも一見すると)。
ファビオはモニカを復活させる '20

1
その答えは元の問題を解決しません。それは単に「宣言を別のヘッダーに入れる」と言うだけです。循環依存関係の解決については何もありません(質問には、ABの定義が利用できるソリューションが必要です。前方宣言では不十分です)。
geza

0

場合によっては、することが可能である定義定義を含む円形の依存関係を解決するために、クラス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

残念ながら、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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.