並ゲームプログラマの書き捨て場

ゲームプログラマやってる私の日記、雑記、備忘録、公開場所

非constexprでcount_of

羊が一匹、羊が二匹・・・六万五千五百三十五匹、零匹・・・眠れない!djannです。

depotというヘッダ提供のテンプレートライブラリを現在、Mercurialリポジトリとして公開しているわけなのだが、git用のリポジトリを別で作成した。こちらはヘッダのみが置いてある、言うなれば使用用のリポジトリとなる。Mercurialよりgitの方がユーザが多いので、そちらで公開することにした。リポジトリはこちら


さて、Visual Studioユーザだとconstexprが使用できるようになるのが2015からということで、depotの中で

#define constexpr

という形で何もしないように定義している。だが結果として、constexpr指定がなされているのに定数として使えないということになる。

これは望ましくないことなので、流石に全ては無理だとしても、例えば配列の要素数を戻すようなものをworkaroundで用意しようかと思っていることが若干、現在の悩みどころである。


さて、本気かどうかわからないのだが、count_ofをstatic_assertつまり定数式に使う方法を寄越せ!ただしconstexprは使用不可の環境でだ。clangやgccは使えない。という連絡が来た。

constexprがないcount_ofをそのまま使うことは出来ないので、ではどのように変数名から配列の要素数を取り出し設定しようかと思ったが、そうなると私にはもう一つしか方法が思い浮かばなかった。幸い完全にC++03でとは言われていない。もちろんsizeof(x) / sizeof(x[0])は定数式になるのだが、流石にこれを看破してしまうとポインタを間違って渡した際に想像もしない挙動になってしまうのでよろしくない。

というわけで、C++11になるまではコンパイル時と言えばこれ。だったテンプレートとC++11で地味に使える機能であるdecltypeを用いることにする。テンプレートの特殊化等に関する基礎練習に向いているので、作ったことがないのなら一旦試してみるのもよいかもしれない。知っているならアーハイハイと流すくらいで丁度良い。



#include <iostream>

template <class vType>
struct count_of;		// dummy.
template <class vType, std::size_t N>
struct count_of<vType[N]> {	// specialize for array.
	enum { value = N };
};

int main()
{
	int a[32];
	static_assert( count_of<decltype(a)>::value < 32, "Compile error!!");
}


まあこんなところだろう。思ったのだが、decltypeが無かったら定数式に使える型安全count_ofは果たして実装出来るのだろうか?私は出来る方法が思いつかない。C++03で実装する方法を知っているという方がいれば、是非教えてほしい。


なお

template <class vType, std::size_t N>
constexpr std::size_t count_of( vType (&)[N] ){ return N; }

と同じ使い勝手にする際は悪のdefineに力を借りて

// 上記テンプレートとは違う名前で.
#define countof(x) count_of<decltype(x)>::value

とすれば無問題・・・なのか?constexprが使える環境になった際はこのマクロをお役御免にしてしまえば、記述は変わらず内容が関数に変更される。他の違いはない・・・のかな?

追記

名無し

VC++の_countofマクロをご存じない?

とのコメントを頂いた。
前に見た時にはその実装はsizeof(foo) / sizeof(foo[0])だったような気がしていたのだが、今回Visual Studio 2012から定義をたどっていくとずいぶん違うものであった。

template <typename _CountofType, size_t _SizeOfArray>
char (*__countof_helper(_UNALIGNED _CountofType (&_Array)[_SizeOfArray]))[_SizeOfArray];
#define _countof(_Array) (sizeof(*__countof_helper(_Array)) + 0)

ひとまずテンプレートを除いて適当な型と数値を割り当てて__countof_helperを記述してみると

char (*__countof_helper( int (&_Arg)[ 10 ] ))[ 10 ];

というような形になる。見慣れない構文にも程があるが、配列char[10]のポインタを戻す関数を定義している、ということだろう。元来から配列のやり取りをするのは非常に奇怪な構文が必要となるC++言語であり、引数部分でも「配列の参照を受け取る」形はこのようになっている。似たようなものだ。そしてそれを呼び出した結果の型をsizeofへ与えている。関数で戻せる型はどのようなものか、呼び出した結果はどのようになるのかを知った方が吸収しやすいと思われるので、そこを踏まえ例を書いた。

int (*fff())[3]{
	static int a[3] = { 0, 1, 2 };
	int (*ret)[3] = &a;	// 長さ3のint型の配列へのポインタを受け取り.
	return ret;	// そして戻す.
}

int main()
{
	// int (*a)[3]と同義である.
	auto a = fff();
	for ( int i = 0; i < 3; ++i ){
		// 0, 1, 2が出力される.
		std::cout << (*a)[i] << std::endl;
	}
	// int *bとなるようだ.
	auto b = *fff();
	for ( int i = 0; i < 3; ++i ){
		// 0, 1, 2が出力される.
		std::cout << b[i] << std::endl;
	}
	// int (*c)[3]となる.
	decltype(fff()) c = fff();
	for ( int i = 0; i < 3; ++i ){
		// 0, 1, 2が出力される.
		std::cout << (*c)[i] << std::endl;
	}
	// int (&d)[3]が結論だ.
	decltype(*fff()) d = *fff();
	for ( int i = 0; i < 3; ++i ){
		// 0, 1, 2が出力される.
		std::cout << d[i] << std::endl;
	}
}

最後のint (&d)[3]となっているところが、_countofマクロ中でsizeofに与えられている型と同様になる。配列の参照をsizeofに渡すことで、配列のサイズが取得できる。__countof_helperはテンプレートであり、引数に与えられた配列の型、要素数が推論される。そしてその推論で導き出された要素数分のchar型配列の参照を戻す。char型は1Byteなので、よってsizeof == countofとなり渡された配列の要素数が取得出来るというカラクリだ。

ポインタにするより、参照を戻す関数*1にした方が、扱いとしてはなじみやすいと思うのだが、そこを配列のポインタを戻す形にしているのは、何かわからない深い意味があってのことなのだろうから、何も言わないようにしておこう。


このカラクリより、関数の呼び出し結果の型をsizeofにコンパイルタイムで渡せるということが、意外とショックを受けた。ならばテンプレート引数にも型として渡せれば、かなり便利だったのに・・・(decltype()はSFINAEとの兼ね合いでVisual Studio上ですこぶる使いづらい。今となってはそれが直ってくれれば良いだけではあるが・・・)。

*1:char (& __countof_helper(...) )[ N ]