事の起こり
C++って、hppファイルやらcppファイルやらを沢山作るせいでエラーが起こった時の対処が大変ですよね。特に大変なのが、クラスが定義されている箇所とそのクラスが宣言や利用されている箇所の関係性の把握です。今回はこのクラスの定義位置関係のエラー2つとその対処を紹介します。
問題のコード
今回の問題となったコードはこちらです(hppファイルである事に注意してください)。本当に簡単なコードですよね。クラスBを持つクラスAが定義されています。
//クラスの前方宣言してもエラーは消えない
class A;
class B;
class A{
public:
B b;//①Field has incomplete type 'B'
B* createBclass(void){//関数定義だけ行う
B *p = new B;//②Allocation of incomplete type 'B'
return p;
}
};
class B{
public:
int num;
};
しかし、この程度のコードで①エラー:Field has incomplete type 'B'と②エラー:Allocation of incomplete type 'B'エラーが2つも出てしまいました!! 最初、私は**「要はクラスAを定義する前にクラスBが定義されていないのが問題でしょ、だったら前方宣言でクラスBの存在を教えてあげれば治るだろ」**と思い、前方宣言を書いたものの全く効果なし。原因は全く別のところにありました。
Field has incomplete typeについて
まずは、Field has incomplete typeについてです。このエラーの原因は、定義が完了されておらず(imcomplete typeですね)、サイズが分からないクラスBを宣言していた事が原因でした。....?????何のことか意味不明ですよね。
順を追って解説します。
①まず、前方宣言によりBという名前のクラスが存在することはhppファイル全体で分かるようになっている
②しかし、クラスBが存在していることは分かっていても、クラスBはクラスAよりも前に定義されていないので、クラスAで宣言した時にクラスBのサイズは定まっていない(クラスBを構築しようにもできない)
③実は、コンパイラはサイズが定まっていないクラスBをどのように配置すればいいのか分からない。
④結果としてエラー発生。
という流れになっています。クラスBのサイズが分からないので構築できない、だから、クラスBをメンバに持つクラスAの構築もできないよということをコンパイラは言っているわけですね。
これを、解消するには次のようにクラスAを修正します。
class A{
public:
B *b;//ポインタにすればOK スマートポインタにすればなお良い
B* createBclass(void){
B* p = new B; //②Allocation of incomplete type 'B
return p;
}
~A(){delete b}//デストラクタでbを解体、これは必須
};
やったことは、クラスB自体をメンバにするのではなく、そのポインタをメンバに変更しただけです。クラスB自体のサイズは未定義なので分かりませんが、メンバbはクラスBを指し示すアドレスです。**あくまで1つのアドレスなのでコンパイラから見てもサイズは明白です。**だから、メンバのサイズが分かればクラスAの構築をコンパイラが理解できるのでエラーが消えたわけです。本当はこれ以外にもクラスAとクラスBを別のhppファイルで定義して、そのhppファイルをお互いにincludeし合うことでも解決可能です。(というよりもそっちが一般的な解決策)
(メンバにクラスのポインタがあるとデストラクタを明示的に書いて、そのポインタが指し示すオブジェクト(B)を解体しないといけません。忘れたらメモリリークです。なので、今回のField has incomplete typeの解決策はあくまでエラーを解消できるだけで得策ではありません.)
Allocation of incomplete typeについて
Allocation of incomplete typeについても、理屈はField has incomplete typeと同じです。Bのサイズ(構成)が分からないから、インスタンス生成なんてできないということですね。この対処としては、クラスのメンバ関数の定義だけをhppファイルでして、実装は別ファイル(cpp)でするという方法を取ります(下記コード)
//クラスの前方宣言
class A;
class B;
class A{
public:
B *b;
B* createBclass(void);//関数定義だけ行う
~A(){delete b}//デストラクタでbを解体、これは必須
};
class B{
public:
int num;
};
# include "myclass.hpp"//①これが大事
B* A::createBclass(void){
B* p = new B;//
return p;
}
**myclass.cppでは最初にクラスA,Bの構成が書かれたmyclass.hppがincludeされているので、クラスA、Bのサイズはmy class.cpp全体で既知なものとなります。**なので、その後のcreateBclass関数で、クラスBのインスタンスを生成できるわけですね。
まとめ
C++では定義完了していないクラス生成はコンパイラができないのでエラーが発生します。このエラーに対応する方法としてクラスのポインタをメンバにする方法を紹介しましたが、やはり、本質的に改善するには、hppとcppにファイルを分ける方法が得策でしょう。hppでクラスの定義は全て済ませておき、そのクラスを使用するファイルでincludeしてやれば、今回のエラーはほとんど防げます。