433

いくつかの悪い設計上の決定(他の誰かによって行われた:))が原因で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;
    }
    
4

11 に答える 11

350

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

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

// 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; 
}

HTH。

于 2009-03-09T21:15:24.577 に答える
115

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

覚えておくべきこと:

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

よくある質問を読む:

于 2009-03-09T11:07:12.400 に答える
15

私はかつて、クラス定義の後にすべてのインラインを移動し、ヘッダーファイルのインライン#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

于 2009-03-09T21:25:01.637 に答える
7

私はこれについて一度投稿を書きました: 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;
}
于 2013-12-15T18:12:47.900 に答える