[C++]コンパイル時と実行時の橋渡し。

 モダンC++では、できるだけコンパイル時に処理をして実行時のコストを軽減する設計が望ましいとされる。それは高速化やポリモーフィズムという観点では確かに正しいのだが、往々にして面倒な状況を生む。例えばstd::tuple<...> tに格納した各値について、実行時変数iを用いてstd::get<i>(t)のように値を得たくなったらどうするのだろう。テンプレート引数に与えることができるのは、コンパイル時に値もしくは型が確定するものだけである。iは実行してみないと分からないので、テンプレート引数には出来ない。std::tupleに限らず、コンパイル時の変数でないと受け付けないテンプレートを実行時に呼び分けなければならない状況はしばしば生じる。

 これを解決する最も単純な方法は、switchなどで分岐する方法だ。

std::tuple<int, double, std::string> t;
int i;
/*
iの値を確定させるような何らかの処理。
*/
switch(i)
{
case 0: std::cout << std::get<0>(t); break;
case 1: std::cout << std::get<1>(t); break;
case 2: std::cout << std::get<2>(t); break;
}

 別にif, else ifでも問題ない。どちらが望ましいかは分岐数やコードの可読性との相談である。
 しかしこの方法には問題がある。std::tupleのテンプレート引数が固定されていない場合だ。例えばもしstd::tupleの各変数が関数の可変長引数として与えられていたりしたら、どうするのか。

template <class ...Args>
void func(Args&& ...args)
{
    auto t = std::forward_as_tuple<Args>(args...);
    int i;
    /*
   iの値を確定させるような何らかの処理。
   */
    switch(i)
    {
    case 0: std::cout << std::get<0>(t); break;
    case 1: std::cout << std::get<1>(t); break;
    case 2: std::cout << std::get<2>(t); break;
    }
}

 この方法は、argsが3個に限られる場合だけ正しく動く。2個以下の場合はコンパイルが通らないし、4個以上では期待した動作にならない。厳密なことを言えばArgsの中にstd::coutのoperator<<がオーバーロードされていない型が入り込んでいたとしてもアウトだが、ややこしいのでここでは考えないことにする。

 この問題を解決する方法はいくつかあるが、ここでは関数ポインタを使った方法を紹介しよう。

namespace detail
{
template<std::size_t N, class Result, template <std::size_t> class Functor, class Tuple, std::size_t ...ArgIndices>
Result TabulationSwitch_impl3(Tuple&& t, std::index_sequence<ArgIndices...>)
{
    return Functor<N>::apply(std::get<ArgIndices>(std::forward<Tuple>(t))...);
}
template<std::size_t N, class Result, template <std::size_t> class Functor, class Tuple, class AIS>
Result TabulationSwitch_impl2(Tuple&& t, AIS ais)
{
    return TabulationSwitch_impl3<N, Result, Functor>(std::forward<Tuple>(t), ais);
}

template<std::size_t SwitchNum, class Result, template <std::size_t> class Functor, class Tuple, std::size_t ...Indices, class AIS>
Result TabulationSwitch_impl(std::size_t n, Tuple&& t, std::index_sequence<Indices...> is, AIS ais)
{
    Result(*func[SwitchNum])(Tuple&&, AIS) =
    {
        TabulationSwitch_impl2<Indices, Result, Functor, Tuple, AIS>...
    };
    return func[n](std::forward<Tuple>(t), ais);
}
}
template<std::size_t SwitchNum, template <std::size_t> class Functor, class ...Args>
decltype(auto) TabulationSwitch(std::size_t n, Args&& ...args)
{
    return detail::TabulationSwitch_impl<SwitchNum, decltype(std::declval<Functor<0>>().apply(std::forward<Args>(args)...)), Functor>(
        n, std::forward_as_tuple(std::forward<Args>(args)...), std::make_index_sequence<SwitchNum>(), std::make_index_sequence<sizeof...(Args)>());
}

template <size_t N>
struct Functor
{
    template <class ...T>
    static void apply(const std::tuple<T...>& t) { std::cout << std::get<N>(t) << std::endl; }
};
template <class ...Args>
void func(Args&& ...args)
{
    auto t = std::forward_as_tuple<Args...>(args...);
    size_t i;
    /*
    iの値を確定させるような何らかの処理。
    */
    TabulationSwitch<sizeof...(Args), Functor>(i, t);
}

 もっとスマートに書けないものかと思うのだが、簡潔に書くとコンパイラによってはエラーを起こしてしまうことがあったため、やや冗長になっている。
 Functor\<N>::applyを各々のNについて予め定義しておいて、それらをTabulationSwitch関数中で関数ポインタ配列に展開し、配列の中から動的にどの関数を呼ぶかを変数iによって選択する。Functorの定義、引数や戻り値の型は任意であるため、殆どの状況では上のTabulationSwitchをコピペして使えるだろう。
 本当はFunctorをラムダ式で定義できるといいのだが、部分特殊化されたFunctorを与えることが難しくなるなど何かと面倒な問題が発生し却って使いづらくなるため私は採用しなかった。一応、数値そのものの代わりに数値を意味する型を作るなど工夫すれば、ラムダ式を与えること自体は可能ではある。

 なおこの方法は、関数ポインタを経由する関係でどうしてもインライン展開が難しい。したがって、億を超える回数の呼び出しがかかったりするとボトルネックになることがある。私は実際にこれがボトルネックになって頭を悩ませたことがある。最終的には、分岐が100個くらいある十分に長い固定長switchをマクロで用意して、各caseが呼ぶ関数オブジェクトの中身をテンプレートで書き換えることにした。これで3倍速くなった。その代わり、分岐数の最大値が生じてしまった。

 Switch関数の実装方法は関数ポインタ以外にもいくらか考えられる。if, else if...を再帰関数で表現したり、unpackする際にi==Nなら関数実行、そうでなければ何もしない、という分岐をさせるなどだ。それらは与えたFunctorが小さく、かつSwitchの分岐数が小さい場合は、関数ポインタよりも高速だろう。どの方法が望ましいかは状況次第。