2017年2月11日土曜日

c++におけるclassの相互参照について

大きなプログラムを長年メンテナンスしていて、改修を繰り返しているとclassの相互参照が必要になってくることがあります。しかも、互いのclassは昔は関係なかったために、別headerで作成していて、相互に#includeしないといけなくなったりします。
ただ、普通にそれをやってしまうとcompilerがやはり「そのclassわからない」というようなエラーを返してきます。

undefined classA
has incomplete classA

とかいってきます。まあ当たり前とは思うんですが、それでもそれを解決しないと先に進みません。色々なキーワードでぐぐってみましたが、やっと理解できたのでメモしておきます。とりあえず、以下にclass Aとclass Bが相互参照しているサンプルを示します。

classA.h
#ifndef CLASSA_H_INCLUDE
#define CLASSA_H_INCLUDE
#include <iostream>
#include "classB.h"
using namespace std;
class B; // 参照の宣言が必要
class A
{
public:
B* b; // class Bはここをコンパイル時点では完全な定義ができていないので、ポインタでしか持てない
A();
~A();
void setUp();
void print();
void printB();
};
#endif
classA.cpp
#include "classA.h"
class B;
A::A()
{
}
A::~A()
{
}
void A::setUp()
{
b = new B;
}
void A::print()
{
cout << "this is classA" << endl;
}
void A::printB()
{
b->print();
}
classB.h
#ifndef CLASSB_H_INCLUDE
#define CLASSB_H_INCLUDE
#include <iostream>
#include "classA.h"
using namespace std;
class A;
class B
{
public:
A* a;
B();
~B();
void setUp();
void print();
void printA();
};
#endif
classB.cpp
#include "classB.h"
class A;
B::B()
{
}
B::~B()
{
}
void B::setUp()
{
a = new A;
}
void B::print()
{
cout << "this is classB" << endl;
}
void B::printA()
{
a->print();
}
main.cpp
#include <iostream>
#include "classA.h"
#include "classB.h"
using namespace std;
int main()
{
A a;
B b;
a.setUp();
b.setUp();
a.printB();
b.printA();
return 0;
}

複数のファイル(classA.h, classA.cpp, classB.h, classB.cpp, main.cpp)で構成されています。この相互参照を可能にするために重要なのが3点あります。

第1点
classA.hの10行目、class B;の参照を宣言しておく。

これはclassA.h内でclass Bを使うために#include "classB.h"をやっているんですが、classA.hをコンパイル時にはまだclass Bのコンパイルは行っていないため、class Bの定義内容がコンパイラにはわかりません。そこで、「とりあえず」class Bというのがあるよ、とプロトタイプ宣言だけしてコンパイラを誤魔化しているようなものです。当然、classB.hでも同じような処置をしておきます。

第2点
class A(class B)の実装は.cppファイルで別に行っておく。

理由はよくわかりませんが、楽しようとしてheaderファイル内にclassの実装もしてしまうとダメなそうです。(大きなプログラムだと大体そうなっているとは思いますが)

第3点
15行目にあるように、相互参照するclassのメンバーはポインタでしか保持できない。

これもコンパイラの問題で、プロトタイプ宣言で当該classがあるよとは誤魔化しましたが、詳細なタイプの定義まではまだこの時点ではわかっていません。そのため、ポインタであれば実体はプログラム実行中に「後で」なんとかできるので、コンパイラはパスします。
あと、重要なのは相互参照しているclassのメンバの実体化は135, 136行目の様にsetUp()関数を使ってやらないといけません。面倒臭がってclass AやBのコンストラクタでnewしてしまうと、実行時に互いをnewしあい、無限loopに入ります。(PCが暴走状態になり、再起動に苦労しました)

大抵こういう時は、調べる情報源にstackoverflow(英語のほうね)が役に立ちますが、今回はぐぐるキーワードでちょっと苦労しました。