[C++] レガシーライブラリをクラスで包んで再利用する

手元にあるものがレガシー(遺産)と言う訳ではないのですが、15年程まえの技術で作ってあるので、これに機能を追加するのに苦労しています。
まあ、業務的には「元のコードスタイルに合わせてコーディングする」のが良い訳で、そこそこ手を加える量が少なければそれでも良いのですが、がっつりとコーディングしないといけないとなると、ひと苦労ってことになります。

そうなると、レガシーならばレガシーなりに捨て去ってしまう、というのもひとつの選択肢に上がるわけですが、現実的にはそうも言えません。予算的な問題とか、実現可能性の問題などがありもともとのコードを継承しないと駄目なわけです。COBOL やホストコンピュータの世界では普通に起こっている話で、ぼちぼち10年前のコード自体も辛くなってきていますよね、って話です。そういう分野の「考察」も必要かと思い始めていますが、そのあたりは別の機会に。

さて、既存のデータが「単純な構造体」と「ビット配列」で出来ていたとします。機能追加をするときには、このデータにアクセスしなければいけないのですが、ばらばらに保存されている「単純な構造体」と「ビット配列」はちょっとアクセスが手間です。あらかじめクラス構造になっていればよいのですが、諸々の事情があってそうもいかなかった模様です。

■データアクセスを #define マクロで包む

ひとつの方法としては「#define」マクロを使ってデータアクセスを包みます。

// conbiniance macro function
#define GRID_MAX()		GETNTGRID()
#define GRID_ID(_idx)		CPLOT2.GPTS[_idx][0]
#define GRID_X(_idx)		CPLOT2.GPTS[_idx][1]
#define GRID_Y(_idx)		CPLOT2.GPTS[_idx][2]
#define GRID_Z(_idx)		CPLOT2.GPTS[_idx][3]
#define TO_GRID_INDEX(_gid)	(_gid-1)

#define CQUAD4_MAX()	GETNTELEM()
static bool IS_CQUAD4(long index) {
	long n = index+1;
	int type = GETELETYPE(&n);
	return ( type == 5 || type == 6 );
}
#define CQUAD4_EID(_idx)	CPLOT2.IDCELM[_idx][4]
#define CQUAD4_GID1(_idx)	CPLOT2.IDCELM[_idx][0]
#define CQUAD4_GID2(_idx)	CPLOT2.IDCELM[_idx][1]
#define CQUAD4_GID3(_idx)	CPLOT2.IDCELM[_idx][2]
#define CQUAD4_GID4(_idx)	CPLOT2.IDCELM[_idx][3]
#define CQUAD4_PID(_idx)	CPLOT2.IDCELM[_idx][5]
#define CQUAD4_K(_idx)	CPLOT2.IDCELM[_idx][5]

見ると分かりますが、CPLOT2.GPTS と CPLOT2.IDCELM は単純な配列です。データ自体は Fortran 領域にあるので「配列」でアクセスしないと難しいという当時の理由がありました。これを直接アクセスするのは大変なので、#define マクロを作ってみたわけです。

これは結構定番な方法なのですが、関数アクセス風になってしまうのが難点です。アクセスする配列が増えるたびに、何らかの命名規約を作ってひとつひとつマクロを作らないといけません。作るのも面倒ですし、アクセスの方法も面倒です。

int idx   = 10;
int eid   = CQUAD4_EID(idx);
int gid1  = CQUAD4_GID1(idx);
int gid2  = CQUAD4_GID2(idx);
...

分かり易いと言えば分かり易いのですが、オブジェクト指向的には次のように書きたいのです。

int idx = 10;
int eid = CQUAD4(idx).EID;
int gid1 = CQUAD4(idx).GID(0);
int gid2 = CQUAD4(idx).GID(1);
...

更に参照を使って、こんな風に書きたい訳です。

int idx = 10;
auto &el = CQUAD4(idx);
int eid  = el.EID;
int gid1 = el.GID(0);
int gid2 = el.GID(1);
...

■データアクセスを一時オブジェクトを使って包む

C# の場合は、別に CQUAD4 クラスを作成してひとつひとつ作り込むことになるのですが、C++ の場合はもうちょっと便利な方法があります。
基本的に一時オブジェクト扱いで使うようにしてスコープが外れると自動的に解放される、という前提でクラスを作ります。C# の場合は、using を使うところですね。

///<summary>
/// CPLOT2.GPTS easy access class
///</summary>
class GRID {
protected:
	int _idx;
public:
	GRID(int idx) { _idx = idx; }
public:
	int GID() { return CPLOT2.GPTS[_idx][0]; }
	double X()  { return CPLOT2.GPTS[_idx][1]; }
	double Y()  { return CPLOT2.GPTS[_idx][2]; }
	double Z()  { return CPLOT2.GPTS[_idx][3]; }
	static int Size() { return GETNTGRID(); }
};

///<summary>
/// CPLOT2.IDCELM easy access class
///</summary>
class CQUAD4 {
protected:
	int _idx;
public:
	CQUAD4(int idx) { _idx = idx; }
public:
	int EID() { return CPLOT2.IDCELM[_idx][4]; }
	int GID(int n) { return CPLOT2.IDCELM[_idx][n]; }
	int PID() { return CPLOT2.IDCELM[_idx][5]; }
	int K() { return CPLOT2.IDCELM[_idx][5]; }
	static int Size() { return GETNTELEM(); }
};

それぞれのメソッドは #define マクロの時と変わりません。

int idx = 10;
auto &el = CQUAD4(idx);
int eid  = el.EID();
int gid1 = el.GID(0);
int gid2 = el.GID(1);
...

一見、プロパティのように見えますがメンバアクセスのメソッドです。実は、VC++の場合は独自にプロパティが作れるのですが、今回はやめておきます。プロパティを作りたい場合は、下記な記事で説明してあります。

MS-C++ では __declspec(property()) でプロパティを作れるよ | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3557

内部的に一時オブジェクトが作られるのでスピードが問題ですが、まぁ、そのあたりは大丈夫みたいです。最近の CPU は早いですから。

カテゴリー: C++ パーマリンク