seriruの技術屋ブログ

競技プログラミングやゲーム開発など技術に関することを発信します

再帰テンプレートでN個のポインタを付けるメタ関数を作る

テンプレートメタプログラミングの練習と定着のために再帰テンプレートを使ってN個のポインタをくっつけるメタ関数を作ります。

参考書

C++テンプレートテクニック第2版

https://www.amazon.co.jp/C-%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%83%86%E3%82%AF%E3%83%8B%E3%83%83%E3%82%AF-%E7%AC%AC2%E7%89%88-%CE%B5%CF%80%CE%B9%CF%83%CF%84%CE%B7%CE%BC%CE%B7/dp/4797376686

コード

前述したC++テンプレートテクニックのP106にあるサンプルに using を使用して任意の型を指定できる別名型宣言を追加したものです。
問題があれば記事は消去します。

#include <iostream>
#include <utility>

// 再帰処理を使用して、T型の変数にN個のポインタを付与するメタ関数

template <int N> struct int_ {};
template <bool cond> struct bool_ {};

// tag despatch
struct cont {}; // continue
struct end {}; // end

template < bool Cond >
auto is_end (bool_<Cond>)
-> typename std::conditional<Cond, end, cont>::type;

template < class T, int N >
auto add_pointers_impl( T, int_<N>, cont )
-> decltype(
  add_pointers_impl(
    std::declval<T*>(),
    int_<N - 1>(),
    std::declval<
    decltype(
      is_end( bool_<N - 1 == 0>() 
      )
    )>()
  )
);

template < class T, int N >
auto add_pointers_impl( T, int_<N>, end )
-> T;

template < class T, int N >
auto add_pointers( T, int_<N> )
-> decltype(
  add_pointers_impl (
    std::declval<T>(),
    int_<N>(),
    std::declval<
    decltype(
      is_end( bool_<N == 0>() )
    )
  >()
  )
);

template < class T, int N >
using pow_pointer = decltype(
  add_pointers(
    std::declval<T>(),
    int_<N>()
  )
);

int main() {
  int* p;
  pow_pointer< int, 2 > dp = &p;
}

解説

pow_pointer に型と付加したいポインタの数を指定するとそれに対応した型が返ってきます。

pow_pointerが最初に呼び出しているのは add_pointers という関数になります。
この関数には具体的な実装がありませんが、この関数を decltype で評価することによって返り値の型を得ることができるようになります。したがって、テンプレートメタプログラミングの文脈で使用するにかぎっては実装はいりません。

add_pointers は処理を add_pointer_impl に委譲します。
処理を委譲する際、型の後置宣言を使用しています。
これは参考書によると、複雑な型宣言をするから、らしい。

add_pointer_impl は引数を3つ取るので、add_pointers の返り値にも3つの引数を与える必要があります。
この3つの引数は、ポインタを付与したい型情報、再帰する回数(N)と再帰条件に使用するタグの情報です。

再帰では、再帰終了条件を付ける必要があります。これがないと永遠に関数が呼び出され続けて、スタックオーバーフローを引き起こします。
ここでは、第3引数に空の構造体の名前を入れることで条件を区別しています。また、このように構造体の名前を使用して処理を分岐することをタグディスパッチといいます。
add_pointer_impl では第3引数が cont なら続ける、 end なら型を返して終了です。

もっと詳しく、返り値の宣言を見てみます。

 decltype(
  add_pointers_impl (
    std::declval<T>(),
    int_<N>(),
    std::declval<
    decltype(
      is_end( bool_<N == 0>() )
    )
  >()
  )
);

decltype を使用して add_pointers_impl の型情報を抜き出す形です。
この際、型Tのインスタンスを作るために std::declval<T>() という関数を使用しています。
これは型Tにデフォルトコンストラクタが存在しなくても型情報を渡せるようにするために存在します。(中では T&& を返しているようですがあんまりよくわかってない。)

また、再帰条件の判定には is_end(bool_<T>()) が渡されています。
この実装は以下のようになります。

template < bool Cond >
auto is_end (bool_<Cond>)
-> typename std::conditional<Cond, end, cont>::type;

最初に宣言した bool_ を使用して真理値を渡し、true ならは end, falseならばcont を返します。
その際、std::conditionalを使用しています。

このように N が 0 になると、 最後に

template < class T, int N >
auto add_pointers_impl( T, int_<N>, end )
-> T;

型情報が返され、結果的に T******... が手に入ります。

まとめ

  • decltypeとdeclvalを使用して型を操作するメタ関数を簡単に作れる。
  • 終了条件の判定には、タグディスパッチを使ったメタ関数のオーバーロードが使える。