読者です 読者をやめる 読者になる 読者になる

C++と色々

主にC++やプログラムに関する記事を投稿します。

ローカルクラス

C++では関数内にもクラスを定義することができます。このクラスはローカルクラスと呼ばれ、静的メンバを定義できない、ローカル変数にアクセス出来ない、関数の外からインスタンス化ができない、などの特徴があります。
ローカルクラスの使いどころを2つ紹介します。

インターフェイスの実装

1つめは、ゲーム制作などに使えると思いますが、パラメータ等を指定してインターフェイスの実装をする方法です。*1
サンプル

#include <iostream>
#include <memory>
#include <sstream>

std::ostringstream sout;

class writer
{
public:
	//渡された文字列を何かに書き込む
	virtual void write(const char* str) = 0;
};

//引数に渡されたストリームに文字列を書き込むwriterを実装する
template <class Stream>
std::shared_ptr<writer> create_writer(Stream& stream)
{
	class local : public writer
	{
	public:
		local(Stream& stream)
			: stream_(stream)
		{
		}

		virtual void write(const char* str) override
		{
			stream_ << str;
		}
	private:
		Stream& stream_;
	};
	return std::make_shared<local>(stream);
}

int main()
{
	//標準出力に書き込むwriterを作成
	auto console_writer = create_writer(std::cout);
	//文字列ストリームに書き込むwriterを作成
	auto string_writer = create_writer(sout);

	console_writer->write("hogehoge\n");
	string_writer->write("foofoo\n");
}

実行結果

hogehoge
続行するには何かキーを押してください . . .

このサンプルではストリームというパラメータを用いて、writerインターフェイスを実装しています。
今回の場合、パラメータを参照で渡しているのでパラメータのスコープに注意する必要があります。
できればパラメータは値渡しでコピーできるものが良いと思います。
ゲーム制作などではhpなどのステータスや行動パターンのファンクタをパラメータにしてキャラクターを作る、とかで使えるかもしれません。
ローカルクラスを用いて作成した派生クラス(インターフェイスを実装したクラスは派生クラスとちょっと違う気がしますが、便宜上一括りにしてしまいます)は、実装が関数の外から見えないので、関数という括りで情報の遮蔽(カプセル化)ができています。シンボルの局所性も高められます。そして、作成されたクラスはJavaでいうfinal classとなっています。つまり、ローカルクラスのサブクラスが作れない(意図しないオーバーライドを防げる)ので、安全性が高まります。
これらのメリットは2つめの方法にも当てはまります。

汎用アダプタークラス

2つめは、汎用的なアダプタークラスを作成する方法です。*2

#include <iostream>
#include <memory>

//変換されるクラス。初期化、実行、終了の3つのメンバ関数を持つ
class adaptee
{
public:
	void initialize()
	{
		std::cout << "初期化\n";
	}
	void run()
	{
		std::cout << "実行\n";
	}
	void release()
	{
		std::cout << "終了処理\n";
	}
};

//新しいインターフェイスはctorで初期化、dtorで終了処理を行う
class interface
{
public:
	virtual void execute() = 0;
};

template <class Adaptee>
std::shared_ptr<interface> make_adapter(const Adaptee& a)
{
	class adapter : public interface
	{
	public:
		adapter(const Adaptee& adaptee)
			: adaptee_(adaptee)
		{
			adaptee_.initialize();
		}
		~adapter()
		{
			adaptee_.release();
		}

		virtual void execute() override
		{
			adaptee_.run();
		}
	private:
		Adaptee adaptee_;
	};

	return std::make_shared<adapter>(a);
}

int main()
{
	auto target = make_adapter(adaptee());
	target->execute();
}

実行結果

初期化
実行
終了処理
続行するには何かキーを押してください . . .

このようにしてAdapterパターンを実現することができます。ローカルクラスを用いなくても同じ事は実現出来ますが、前述したとおり、この関数の使用者はadapterの実装を導出する方法がありません。これをグローバル領域で行おうとすると、新しい翻訳単位を用意し、無名名前空間を使う必要があります。

*1:これは自分が勝手に考えた利用方法です

*2:Modern C++ Designに載っていた方法です