クラス間の循環依存によるビルドエラーを解決する
-
05-07-2019 - |
質問
私は、C ++プロジェクトで複数のコンパイル/リンカーエラーに直面している状況にしばしば遭遇します。これは、異なるヘッダーファイルのC ++クラス間の循環依存関係につながる(他の誰かが行った)悪い設計決定によるものです< em>(同じファイルでも発生する可能性があります)。しかし、幸いなことに(?)、これが頻繁に発生するわけではないので、次回にこの問題の解決策を思い出すことができません。
そのため、将来の簡単なリコールのために、代表的な問題とそれに伴う解決策を投稿します。より良いソリューションはもちろん歓迎です。
-
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; }
解決
これについて考える方法は、<!> quot;コンパイラのように考える<!> quot;です。
あなたがコンパイラを書いていると想像してください。そして、このようなコードが表示されます。
// 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 がコンパイルの単位であることに注意してください)、オブジェクトA
にスペースを割り当てる必要があります。それで、それでは、どのくらいのスペースですか? B
を保存するのに十分です!では、main()
のサイズは? #include
を保存するのに十分です!おっと。
明らかに、参照する必要がある循環参照。
コンパイラーが代わりに前もって知っている限りのスペースを確保できるようにすることで、これを破ることができます-例えば、ポインターと参照は常に32または64ビット(アーキテクチャーに依存)であり、置き換えた場合(どちらか1)ポインタまたは参照によって、物事は素晴らしいだろう。 #include "A.h"
:
// file: A.h
class A {
// both these are fine, so are various const versions of the same.
B& _b_ref;
B* _b_ptr;
};
今では物事が良くなっています。幾分。 <=>まだ言う:
// file: main.cc
#include "A.h" // <-- Houston, we have a problem
<=>、すべてのエクステントおよび目的(プリプロセッサを取り出す場合)で、ファイルを .cc にコピーするだけです。本当に、 .cc は次のようになります:
// file: partially_pre_processed_main.cc
class A {
B& _b_ref;
B* _b_ptr;
};
#include "B.h"
int main (...) {
A a;
}
コンパイラがこれを処理できない理由を見ることができます-<=>が何であるか見当がつきません-シンボルを見たことがありません。
では、コンパイラに<=>について伝えましょう。これは前方宣言として知られており、この回答。
// main.cc
class B;
#include "A.h"
#include "B.h"
int main (...) {
A a;
}
これは動作します。それは素晴らしいではありません。しかし、この時点で、循環参照の問題と、<!> quot; fix <!> quot;に対して行ったことを理解する必要があります。修正は悪いものの、それ。
この修正が悪い理由は、<=>の次の人が使用する前に<=>を宣言する必要があり、ひどい<=>エラーを受け取るためです。宣言を A.h 自体に移動しましょう。
// file: A.h
class B;
class A {
B* _b; // or any of the other variants.
};
そして B.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。
他のヒント
ヘッダーファイルからメソッド定義を削除し、クラスにメソッド宣言と変数宣言/定義のみを含めると、コンパイルエラーを回避できます。メソッド定義は.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;
}
覚えておくべきこと:
-
class A
がclass B
のオブジェクトをメンバーとして持つ場合、またはその逆の場合、これは機能しません。 - 前方宣言が進むべき道です。
- 宣言の順序が重要です(定義を削除する理由です)。
- 両方のクラスが他のクラスの関数を呼び出す場合、定義を外に移動する必要があります。
FAQを読む:
これに答えるのが遅くなりましたが、非常に支持された回答を伴う一般的な質問であるにもかかわらず、これまでのところ合理的な答えはありません。...
ベストプラクティス:前方宣言ヘッダー
標準ライブラリの<iosfwd>
ヘッダーで示されているように、他のユーザーに前方宣言を提供する適切な方法は、 前方宣言ヘッダー を持つことです。例:
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
ライブラリのメンテナーはそれぞれ、ヘッダーと実装ファイルと順方向宣言ヘッダーを同期させる責任があります。たとえば、<!> quot; B < !> quot;一緒に来て、コードを次のように書き直します...
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;
... <!> quot; A <!> quot;のコードの再コンパイル含まれているb.fwd.h
への変更によってトリガーされ、正常に完了する必要があります。
悪いが一般的な慣行:他のライブラリで前方宣言のもの
言う-上記で説明したように前方宣言ヘッダーを使用する代わりに-a.h
またはa.cc
のコードで代わりにclass B;
自体を前方宣言します:
-
b.h
または#include
に後で<=>が含まれていた場合:- 競合する宣言/定義<=>に到達すると、Aのコンパイルはエラーで終了します(つまり、上記のBへの変更により、透過的に動作する代わりに、前方宣言を悪用するAと他のクライアントが破損しました)。
- それ以外の場合(Aが最終的に<=>を含まなかった場合-AがBをポインタおよび/または参照によって単に格納/渡す場合のみ可能)
- <=>分析と変更されたファイルのタイムスタンプに依存するビルドツールは、Bへの変更後に<=>(およびさらに依存するコード)を再構築せず、リンク時または実行時にエラーを引き起こします。 BがランタイムロードDLLとして配布されている場合、<!> quot; A <!> quot;のコード実行時に異なるマングル記号を見つけることができない場合があります。これは、正常なシャットダウンまたは許容可能な機能低下を引き起こすほど十分に処理されない場合があります。
Aのコードにテンプレートの特殊化がある場合/ <!> quot; traits <!> quot;古い<=>の場合、それらは有効になりません。
クラス定義の後にすべての inline を移動し、ヘッダーファイルの 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
このことについて一度投稿しました: 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;
}
テンプレートのソリューションは次のとおりです。テンプレートを使用して循環依存関係を処理する方法
この問題を解決する手がかりは、定義(実装)を提供する前に両方のクラスを宣言することです。 <!>#8217;宣言と定義を別々のファイルに分割することはできませんが、別々のファイルにあるかのように構造化できます。
ウィキペディアに掲載されている簡単な例がうまくいきました。 (完全な説明は、 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;
}
残念ながら、以前の回答にはすべて詳細が欠けています。正しい解決策は少し面倒ですが、これが適切にそれを行う唯一の方法です。また、簡単に拡張でき、より複雑な依存関係も処理します。
これを行う方法は次のとおりです。すべての詳細と使いやすさを正確に保持します。
- 解決策は当初意図したものとまったく同じです
- インライン関数はまだインライン
-
A
およびB
のユーザーは、A.hおよびB.hを任意の順序で含めることができます
A_def.h、B_def.hの2つのファイルを作成します。これらには、<=>と<=>の定義のみが含まれます。
// 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は<!> quot; private <!> quot;であることに注意してください。ヘッダー、<=>および<=>のユーザーは使用しないでください。公開ヘッダーはA.hおよびB.hです。
場合によっては、クラス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();
}
残念ながら、gezaからの回答にコメントすることはできません。
彼は単に<!> quot;宣言を別のヘッダーに転送するだけではありません<!> quot;。彼は、クラス定義ヘッダーとインライン関数定義を異なるヘッダーファイルにこぼして<!> quot; defered依存関係<!> quot;。
を許可する必要があると言います。しかし、彼のイラストはあまり良くありません。両方のクラス(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つのヘッダーファイルで正確に可能です。 唯一の問題は、循環クラスに<!> quot; public header <!> quot;を含めることができないことです。
この問題を解決するには、プリプロセッサ拡張機能を提案したいと思います。#pragma process_pending_includes
このディレクティブは、現在のファイルの処理を延期し、保留中のすべてのインクルードを完了する必要があります。