shared_ptrとHDCとSelectObjectと

「HDCやSelectObjectしたHGDIOBJの寿命管理をするスマートな方法が無いか」という独り言に勝手に答えてみました。

Motivation

  • shared_ptrをマンセーする
  • ctor/dtorのためだけのクラスを作りたくない

Approach

  • Win32 HANDLEをboost::shared_ptr (or std::tr1::shared_ptr)で管理する
  • 他の方法での寿命管理は行なわない

Preparation

includeとかtypedefとか。

#include <windows.h>
#include <boost/shared_ptr.hpp>
#include <cassert>


/// HWNDの実体の名前を取り出すメタ関数
template <typename T>
struct deref_ptr;
template <typename T>
struct deref_ptr<T *> { typedef T type; };

using boost::shared_ptr; //< std::tr1::shared_ptrでもいいはず
typedef shared_ptr<deref_ptr<HWND>::type> shared_hwnd;
typedef shared_ptr<deref_ptr<HGDIOBJ>::type> shared_hgdiobj;
typedef shared_ptr<deref_ptr<HDC>::type> shared_hdc;
typedef shared_ptr<deref_ptr<HPEN>::type> shared_pen;
typedef shared_ptr<deref_ptr<HBRUSH>::type> shared_brush;
typedef shared_ptr<void> selobj_duration; //< これが今回の主役

typedef COLORREF color_t;

/// おまけ。
inline bool line_to(shared_hdc dc, int x, int y) { return LineTo(dc.get(), x, y); }

deref_ptrはきっとboost探せばあるだろうけど、面倒コンパイル遅くなりそうなので自前で用意。
実際はcolor_tももう少し真面目に作ったほうがよさそう(移植性的な意味で)。

Factories/Deleters

ファクトリ・デリータたち。

/// デバイスコンテキストを取ってくる
inline shared_hdc get_dc(shared_hwnd h) {
	return shared_hdc(GetDC(h.get()), release_dc_fn(h));
}
/// ペンを作る
inline shared_pen create_pen(int style, int width, color_t color) {
	return shared_pen(CreatePen(style, width, color), DeleteObject);
}
/// ブラシを作る
inline shared_brush create_solid_brush(color_t color) {
	return shared_brush(CreateSolidBrush(color), DeleteObject);
}
/// GetDCで取ったHDCを殺す
struct release_dc_fn {
	release_dc_fn(shared_hwnd hw_): hw(hw_) { }
	void operator ()(HDC dc) { ReleaseDC(hw.get(), dc);}
	shared_hwnd hw;
};

これもrelease_dc_fnはboost::bindとか使えそうだけど、コンパイル時間ry

Principal

今日の主役。
簡単に言うと、"SelectObjectされた状態"を寿命管理するという考え方。
selobj_duration自体には"生きている"以上の価値はありません。
selobj_durationが死ぬと、"SelectObjectされた状態"が終わったとみなし、
元のHGDIOBJでSelectObjectをしなおします。

/// selobj_durationが死んだときに元のHGDIOBJにSelectObjectしなおす
struct rollback_select_object_fn {
	rollback_select_object_fn(shared_hdc dc_, shared_hgdiobj cur_): dc(dc_), cur(cur_) { }
	void operator ()(HGDIOBJ old) { assert(cur.get() == SelectObject(dc.get(), old)); }
	shared_hdc dc;
	shared_hgdiobj cur;
};
/// 新しいHGDIOBJにSelectObjectして、元のHGDIOBJ・shared_hdc・shared_hgdiobjを持ったselobj_durationを返す
inline selobj_duration select_object(shared_hdc dc, shared_hgdiobj obj) {
	HGDIOBJ old = SelectObject(dc.get(), obj.get());
	return shared_hgdiobj(old, rollback_select_object_fn(dc, obj));
}

selobj_durationのDeleterにshared_hdcを持たせることで、勝手に元のHDCが開放されないことを保障しています。
また、新しいHGDIOBJを持っているshared_hgdiobjを持たせてassertをかけていますが、これは状況によっては邪魔かもしれません。
これもboost::bindで出来る、はず。きっと。

Usage

int main() {
	shared_hdc dc = get_dc(shared_hwnd());
	{
		selobj_duration sel0;
		{
			shared_pen pen1 = create_pen(PS_SOLID, 5, RGB(0x20, 0x40, 0x60));
			shared_pen pen2 = create_pen(PS_SOLID, 10, RGB(0x60, 0x40, 0x20));
			shared_brush brush = create_solid_brush(RGB(0x80, 0xA0, 0xC0));
			{
				selobj_duration sel1 = select_object(dc, pen1); // a
				line_to(dc, 100, 100);
			} // A
			line_to(dc, 200, 200);
			sel0 = select_object(dc, pen2); // b
			line_to(dc, 300, 300);
		} // B
		line_to(dc, 400, 400);
	} // C
	line_to(dc, 500, 500);
} // D

ctor/dtorのログを取れるようにするのが面倒だったのでやってませんが、
a地点でpen1にSelectObjectされたものが、A地点で元に戻り、2回目のline_toではデフォルトのペンに戻っています。
また、b地点でpen2にSelectObjectしていますが、sel0の寿命はC地点まで残っているため、4回目のline_toの時点でもpen2は生き残っていて、これを使って描画しています。
そして、C地点にたどり着くとsel0も消えるため、5回目のline_toはデフォルトのペンで描画します。

このようにペンやブラシの寿命が先に来ても、SelectObjectされている限り勝手に消えることがなくなり、神経質に寿命管理をしなくてもよくなりました。
shared_ptrってすばらしい。
shared_ptr最高です。
shared_pt(ry