1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ゲームプログラマのための設計:事前処理・事後処理の強制方法比較

Last updated at Posted at 2024-11-14

ゲームプログラマのための設計シリーズ:デザインパターン編の記事です。

概要

  • 事前処理・事後処理を確実に呼び出したいなら、RAII・ローンパターン・Template Methodパターンが考えられる
  • スコープ外に持ち出す必要があるならRAII
  • そうでなければローンパターンがオススメ

本文

こんなAPIがあるとします。

class Graphics
{
public:
	void beginDraw(); // 描画開始
	void drawXXXX();  // 具体的な描画
	void drawYYYY();  // 具体的な描画
	void endDraw();   // 描画終了
};

beginDraw/endDrawが「具体的な描画」関数の呼び出し前後に確実に呼ばれるようにするにはどうしたらよいでしょうか。

RAII

まずは、おなじみのRAIIです。

class RAIIDrawer final
{
public:
	explicit RAIIDrawer(Graphics& g) : mGraphics(g) { mGraphics.beginDraw(); }
	~RAIIDrawer() { mGraphics.endDraw(); }

	// ※コピーやムーブの実装は要注意。必要なければ消しておくのがオススメ
	RAIIDrawer(const RAIIDrawer&) = delete;
	RAIIDrawer(RAIIDrawer&&) = delete;

	// begin/endの間に行ってほしい操作へのプロキシ
	void drawXXXX() { mGraphics.drawXXXX(); }
	void drawYYYY() { mGraphics.drawYYYY(); }

private:
	Graphics& mGraphics;
};
void test(Graphics& graphics)
{
	RAIIDrawer drawer(graphics);  // DrawerのコンストラクタでbeginDraw

	drawer.drawXXXX();
	drawer.drawYYYY();

	// DrawerのデストラクタでendDraw
}

この方法はRAIIオブジェクトをスコープの外に持ち出せるメリットがあります。
が、それゆえに

  • RAIIオブジェクトのコピー/ムーブの実装には気を使わないといけない
  • RAIIオブジェクトより先にGraphicsの寿命が尽きてしまい不正な参照となる

という危険も存在します。

ローンパターン

もう一つの解は、高階関数を使う方法です。

// beginDraw/endDrawを隠す
class DrawProxy
{
public:
	explicit DrawProxy(Graphics& g) : mGraphics(g) {}

	// begin/endの間に行ってほしい操作へのプロキシ
	void drawXXXX() { mGraphics.drawXXXX(); }
	void drawYYYY() { mGraphics.drawYYYY(); }
private:
	Graphics mGraphics;
};

void LoanDraw(Graphics& graphics, std::function<void(DrawProxy&&)> f)
{
	graphics.beginDraw();

	f(DrawProxy{ graphics });

	graphics.endDraw();
}
void test2(Graphics& graphics)
{
    // LoanDrawがbegin/endDrawを呼んでくれる
	LoanDraw(graphics, [](DrawProxy&& proxy) {
		proxy.drawXXXX();
		proxy.drawYYYY();
	});
}

良くも悪くもスコープの外に処理を持ち出せないので、その必要のないケースでは安心です。

Template Methodパターン

継承で解決する方法も考えられます。

class DrawerBase
{
public:
	explicit DrawerBase(Graphics& g) : mGraphics(g) {}
	void draw()
	{
		mGraphics.beginDraw();
		drawImpl_();  // Template Methodパターン
		mGraphics.endDraw();
	}

protected:
	// drawImpl_から呼んでもらう用 
	// (ローンパターンにおけるDrawProxyのようにしてもいい)
	void drawXXXX_() { mGraphics.drawXXXX(); }
	void drawYYYY_() { mGraphics.drawYYYY(); }

private:
	virtual void drawImpl_() = 0;
	Graphics mGraphics;
};
void test3(Graphics& graphics)
{
	class Drawer : public DrawerBase
	{
	public:
		explicit Drawer(Graphics& g) : DrawerBase(g) {}
	private:
		void drawImpl_() override
		{
			drawXXXX_();
			drawYYYY_();
		}
	};

	Drawer{ graphics }.draw();
}

こちらは処理の数だけDrawerBase継承を増やさないといけないのがちょっと大がかりすぎますね。

まとめ

スコープ外持ち出し 利用側の記述量
RAII
ローンパターン ×
Template Methodパターン ×
  • スコープ外に持ち出す必要があるならRAII
  • そうでなければローンパターンがオススメ

です。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?