コピーとBridgeパターン
グラフィックスライブラリを作る
ゲームを作るのに欠かせないグラフィックス。BASICではやり方のバリエーションはあまりなくって悩むところも無かったのですが、 WindowsではGDIを扱わなきゃならないとか、 透明色を扱うにはDIBSectionがいいとか、 ビットマップファイルを読み込まなきゃいけないとか複雑です。 いまはDirectXまであります。
しかもGDIとDIBSectionはハンドルで、 ビットマップファイルはポインタを自前で管理、 DirectXはCOMオブジェクトを管理しなければならないと、 もう混沌としています。
実はWindowsでC++プログラミングを始めたきっかけというのも、 こんな複雑なグラフィックスを簡単に扱えるようライブラリを構築しようと考えたからなのです。もっとも最初はGDIとWinGくらいでしたけど。
しかしいざ作り始めてみると、 使い勝手や効率は気になるし、 新しいテクニックを覚えたら使いたくなるし、 DirectXなどの最新技術が出てくれば対応したくなるしで、 作ってはまたやり直し作ってはやり直しでなかなか納得のいくものはできませんでした。いまや、私のプログラミングライフの中心と言っても過言ではないほどです。
頭の痛い「コピー」問題
WinGLというライブラリと出会ったのをきっかけに、 いわゆる「ビットマップ」を中心とした組み立てに方針が固まりました。DIBSectionもビットマップファイルもDirectXも「ビットマップ」を仲介することにすればインターフェースが統一できるので、 継承を利用してクラスツリーを組み立てられます。
さらに描画元(pattern)と描画先(canvas)を分けて考えることで、 機能的な違いもうまく構造化できました。こんな感じで使います。
#include <moo/draw.h>
using namespace moo;
void main()
{
bitmap bmp;
bmp.load("doggie.bmp"); // doggie.bmpなるファイルをロード
bitmap out;
out.allocate(400, 300, 24); // 400x300x24のメモリビットマップを用意
ml_pattern mp(bmp); // ビットマップを描画先として使う=スプライト
ml_pattern np(bitmap(bmp, 100, 0, 140, 180)); // 部分的にスプライト化
fast_pattern fp(bmp, 0); // bmpのピクセル値0を透明にしたスプライト
mp.put(out.canvas(), 0, 0); // outの( 0,0)にmpを描画
np.put(out.canvas(), 150, 0); // outの(150,0)にnpを描画
fp.put(out.canvas(), 150, 0); // outの(150,0)にfpを描画
out.save("out.bmp"); // outをファイルとして保存
}
左:doggie-r.bmp、右:out.bmp(2分の1に縮小&256色に減色してあります)ちなみにこれはWinGのサンプルについていた画像(^_^)
とはいえ、現在のクラス構造を得るまで長年かかったのですが、 ようやく安定しそうです。
一方、悩みに悩んだのがコピー問題。C++的な代入をどう扱おうか、悩み始めると夜も眠れません。
こんなコードを考えてください。
ml_pattern mp(なになに);
ml_pattern np = mp;
npには何が入るべきでしょう?
もちろん代入演算は等号を介して行われるのですから、 同じものであることが期待されて当然です。
じゃぁ、内部はどうせポインタで管理しているのだから、 ポインタをコピーすればいいじゃん!しかし、管理を誰がするのかで悩みます。 mpとnpのどちらが削除の責任を持てばいいのでしょうか。
次のような関数の返値として使うときも問題です。
ml_pattern generate()
{
return ml_pattern(なになに);
}
void func()
{
ml_pattern mp = func();
}
func()で作成されたml_patternはmpに代入されますが、 ポインタはコピーされていてもそのポインタが指すアドレスは既に向こうになっているかも知れません。
それならば、メモリを毎回新たに確保して内容をコピーすれば?大きなパターンの場合や大量にある場合にコストが高くつきすぎることは、 容易に想像がつきます。また、描画先の場合は、コピーしてもやはり元と同じものを対象としたいですが、 内容をコピーしてしまうとそれができません。
void func()
{
fast_pattern fp(なになに);
bitmap out;
out.allocate(400, 300, 24);
canvas c = out.canvas(); // コピー
fp.put(c, 20, 30); // fpは何に描画するの?
}
ちなみに内容まで完全に複製するコピーを「深い(deep)」コピーというそうです。共有コピーを使おう
ml_patternやfast_patternは、patternの派生クラスでありまして、 それならばポリモルフィズム(多態性、同じインターフェースを通じて異なる挙動をすること)を使いたいところです。
void func(const pattern& p)
{
p.put(なにか.canvas(), 0, 0);
}
void main()
{
fast_pattern fp(なになに);
func(fp); // patternが必要なところに、fast_patternを渡す
}
これだけなら当然できますが、次は悩みます。
void main()
{
vector<pattern> list;
fast_pattern fp(なになに);
list.push_back(fp); // コピーが必要
}
pattern*のリストにすることもできましょうが、メモリ管理が難しいので避けたいところ。
そこでたどり着いたのが次の設計。
class pattern
{
public :
class pattern_impl
{
public :
typedef size_t size_type;
virtual ~pattern_impl()
{
#ifdef DEBUG
std::cerr << "D[pattern_impl].\n";
#endif
}
virtual size_type width() const = 0;
virtual size_type height() const = 0;
virtual size_type bitspixel() const = 0;
virtual pattern_impl* clone() const = 0;
virtual operator bool() const = 0;
virtual bool put(canvas& c, int x, int y, int w, int h) const = 0;
private :
};
typedef pattern_impl::size_type size_type;
typedef boost::shared_ptr<pattern_impl> impl_type;
pattern(const pattern& rhs)
: _impl(rhs._impl)
{};
pattern() {}; // 派生クラス専用:必ず_implをセットせよ
virtual ~pattern()
{
#ifdef DEBUG
std::cerr << "D[pattern].\n";
#endif
}
size_type width() const { return _impl ? _impl->width() : 0; }
size_type height() const { return _impl ? _impl->height() : 0; }
size_type bitspixel() const { return _impl ? _impl->bitspixel() : 0; }
// clone : 実装を「深く」コピーする。
// これで作られた場合、ポインタは共有されない。
const pattern clone() const
{ return _impl ? pattern(_impl->clone()) : pattern(); }
operator bool() const { return _impl.get() ? *_impl : false; }
bool put(canvas& c, int x, int y, int w = -1, int h = -1) const
{ return _impl ? _impl->put(c, x, y, w, h) : false; }
protected :
impl_type _impl;
pattern(pattern_impl* impl)
: _impl(impl_type(impl)) {};
void set_impl(pattern_impl* impl) { _impl = impl_type(impl); }
};
完全な pattern / ml_pattern、 fast_pattern はこちらを。
ポイントは強調部分。patternは実装クラスの共有ポインタとそこへのインターフェースを持つだけ、 というものです。実装オブジェクトはnewして格納します。 コピーや代入は原則として実装オブジェクトのポインタのコピーですが、 共有カウンタ付きスマートポインタ(boost::shared_ptr)を使って管理問題を解決しています。
ml_patternやfast_patternは実装のジェネレータとして機能します。ml_patternはメモリビットマップでの実装を生成し、 fast_patternは透明色付きにエンコードしたスプライトでの実装を生成します。patternへの代入も実装オブジェクトの共有コピーです。
メモリ内容まで複製するディープコピーをしたい場合は、 clone()を明示的に使います。もちろん複製の方法も実装毎に違うので実装オブジェクトのclone()を呼びます。
まとめると、 インターフェースと実装を分けた、 共有コピーを使った、 というのがポイントでしょうか。 お互いの利点を活かしあう設計だと自負しています。
デザインパターンによれば・・・
ここまでできあがったところで、 こんなデザインパターンは無いのかとGoF本を開いてみました。よくよく読むとBridgeパターンというのが該当します。
なーんだ、早く気づけばよかった・・・とも思いましたが、 ここは確かに読んだことはあったし、 自分でつくってみてやっと使い方&利点が分かったというのが本音です。
関連するデザインパターンにはAbstract Factoryパターンがありましたが、 ml_pattern/fast_patternが該当することになります。もっとも全然abstractでは無いんですけど(^^ゞ
さて、Bridgeパターンの実装上の注意の覧には Implementor(実装オブジェクト)の共有というのがあって、 やはり共有カウンタを使っていました。また、使用例の覧にはNeXT(!)のライブラリのグラフィック部分が紹介されていました。やはり、こういう用途にはBridgeパターンにたどり着くのか、 と思った次第です。
ともあれ、我がライブラリのグラフィックス部分はほぼ解決。でもGoF本の例でクロスプラットフォームなウィンドウシステムが出ていたのが、 ちょっと私の心をくすぐったりしているのでした(^^ゞ
