関数にstd::initializer_list
を渡した時にサイズに依存してメタプログラミングしたい事がある。
典型的には、特定の要素数のみを受け取りたい時など。
まちがい
まず、私が犯した過ちは
constexpr void f (std::initializer_list<int> list)
{
static_assert(list.size() == 3, ""); // error - listはconstexprじゃない
}
int main ()
{
f({1, 2, 3}); // OKになってほしい
f({1, 2}); // NGになってほしい
}
std::initializer_list<T>::size()
はC++14からconstexpr指定されるが、fの定義内では、listはconstexprに見えないので、コンパイルエラーになる。
constexpr関数はconstexprでない文脈でも呼ばれる可能性があるので、constexpr関数内で引数がconstexprであることを前提にしたコードは書けない。
対策
基本的に、initializer listの構文(brace-init-list)を使って関数の実引数にstd::initializer_list
を作ってしまうとサイズをconstexprで取り出すことはできない。
ところで、関数呼び出しにinitializer listの構文を使う際は、リスト初期化の方法によって引数が初期化される。
また、引数の初期化の順番は本の虫に書いてある通り。
重要な事は、引数がクラスで、initializer listが空でなく、引数の型が初期化リストコンストラクタを持たない場合、initializer listは実引数リストとみなされ、展開される。
つまり
struct foo
{
template <typename ... Args>
foo (Args ... args) // これが呼ばれる。
{
}
};
void f (foo) {}
int main ()
{
f({1, 2, 3});
}
となる。
これを使えば、sizeof...(Args)
で関数呼び出しの際の初期化リストのサイズをconstexprの文脈で取得できる。
ただし、args
をf
の中で使おうとすると、foo
のメンバに渡すしかないが、foo
のメンバの型を決めるのにinitializer listのサイズを使う事はできない。
結局、f
の中でやりたいことをfoo
のコンストラクタに委譲するしかない。
となると最終的に次の様になる。
template <typename Func, typename ReturnType>
struct init_list_proc
{
ReturnType value;
template <typename ... Args>
constexpr init_list_proc (Args && ... args) { value = Func()(std::forward<Args>(args)...); }
};
struct f_impl
{
template <typename ... Args>
constexpr int operator() (Args && ... args) noexcept // ここにやりたい処理を書く
{
static_assert(sizeof...(Args) == 3, "");
return 1;
}
};
int f (init_list_proc<f_impl, int> proc)
{
return proc.value;
}
int main ()
{
f({1, 2, 3}); //OK
f({1, 2}); // NG
}
条件でf
の型を変えたい場合はf_impl::operator()
をオーバーロードして、f
の方もオーバーロードすれば良い。
ただし、これでもまだ、f
の返り値の型をメタプログラミングで導出する事はできない。
もっと簡単な方法がある気もする。