Edited at

可変長引数テンプレートをメタ配列として扱う part1

More than 3 years have passed since last update.

今回は可変長引数テンプレート(variadic template)の話です。


std::integer_sequence

C++14で新しくstd::integer_sequenceというテンプレートクラスが追加されました。


c++

namespace std {

template<typename T, T ...Ints>
class integer_sequence {
public:
using value_type = T;
static constexpr size_t size() noexcept { return sizeof...(Ints); }
};

template<size_t ...Ints>
using index_sequence = integer_sequence<size_t, Ints...>;

template<typename T, T N>
using make_integer_sequence = std::integer_sequence<T, /* 0, 1, 2, 3, ..., N */ >;

template<size_t N>
using make_index_sequence = make_integer_sequence<size_t, N>;

template<class... T>
using index_sequence_for = std::make_index_sequence<sizeof...(T)>;

}


これを利用すれば、タプルを展開して関数の引数にするなんてことができます。


c++


template<typename F, typename Tuple, std::size_t ...Ints>
void invoke_(F&& f, Tuple&& t, std::index_sequence<Ints...>) {
using std::get;
f(get<Ints>(t)...);
}

template<typename F, typename Tuple>
void invoke(F&& f, Tuple&& t) {
using std::tuple_size;
using indices = std::make_index_sequence<tuple_size<Tuple>::value>;
invoke_(std::forward<F>(f), std::forward<Tuple>(t), indices{});
}

void func(char, int, long long) {
// ...
}

int main() {
auto t = std::make_tuple('\0', 0, 0ll);
invoke(func, t); // func('\0', 0, 0ll) が呼ばれる
}


C++14で追加されたとは言え、コア言語に何かが追加された訳ではなく単に標準ライブラリ入りしただけなので、C++11の機能でも作成可能です。

上記の例におけるinvokeのような事をやりたい時は、C++11では同じようなクラスを作らざるをえません。

まあ、そろそろC++14使おうぜって話で終わりたいところですが。

趣味でやる分にはそれでもいいんですが……、仕事ではなかなか使わせてもらえない人も多いでしょう。主に、RHEL6とか、VC++とかのせいで。

私なんかC++11すら使わせてもらえません。


variadic templateの種類

variadic templateには二種類あります。一つ目は、


A

template<型名 ...仮引数名>


と書くもの。もう一つは、


B

template<typename ...仮引数名>

template<class ...仮引数名>

と書くもの。

template<template<class ...> class ...仮引数名>

template<template<template<class ...> class ...> class ...仮引数名>

なんてものも作ろうと思えば作れるのでネストを考えればいくらでもパターンは増やせるのですが、さすがにそんなことまで考えても仕方ありません。

AとBの違いは、コンパイル時定数を渡すか、型を渡すか、という点です。

今回はとりあえず、std::integer_sequenceの機能拡張版のような、定数バージョンのクラスを考えます。


定数の配列を扱うクラス

std::integer_sequenceの機能拡張版なので、名前もinteger_sequenceにしましょう。ただし名前空間は分けます。

一方、型の配列を扱うクラスは、type_sequenceにします。こちらに関しては次回の記事で扱いたいと思います。

長々と説明をするより見た方が早そうなので、早速コードを見てみます。


c++

namespace geranium  {

// C++14のstd::enable_if_tと同じ
template<bool cond, typename T = void>
using enable_if_t = typename std::enable_if<cond, T>::type;

template<bool cond>
using enable_when = enable_if_t<cond, std::nullptr_t>;

// #define GERANIUM_ENABLE_WHEN(cond) ::geranium::enable_when<(cond)> = nullptr
// みたいなマクロがあってもいいかも知れないけど、マクロだしなあ……

// 前方宣言
template<typename ...Args>
class type_sequence;

// 前方宣言
template<typename T, T ...args>
class integer_sequence;

// Uが型Tのinteger_sequenceかどうか調べる
template<typename T, typename U>
struct is_integer_sequence: std::false_type {};
template<typename T, T ...args>
struct is_integer_sequence<T, integer_sequence<T, args...>>: std::true_type {};
template<typename T, typename U>
using is_integer_sequence_t = typename is_integer_sequence<T, U>::type;

// std::index_sequenceと同じ
template<std::size_t ...args>
using index_sequence = integer_sequence<std::size_t, args...>;

// std::make_integer_sequenceと少し異なり、n~n+c-1の範囲で作成する
template<typename T, T n, std::size_t c>
using make_integer_sequence = std::integer_sequence<T, /* n, n+1, n+2, ..., n+c-1 */ >;

// std::make_index_sequenceと同じ
template<std::size_t n>
using make_index_sequence = make_integer_sequence<std::size_t, 0, n>;

// std::index_sequence_forと同じ
template<class... Args>
using index_sequence_for = std::make_index_sequence<sizeof...(Args)>;

template<typename T, T ...args>
class integer_sequence {
public:
using value_type = T;
using size_type = std::size_t;

static constexpr size_type npos = static_cast<size_type>(-1);
static inline constexpr size_type size() noexcept { return sizeof...(args); }

// テンプレートUを特殊化したクラスを返す
template<template<T ...> class U> using unpack = U<args...>;

// args...それぞれにconstexpr関数を適用して作成したinteger_sequenceを返す
template<typename U, U(f)(T)> using map = integer_sequence<U, f(args)...>;

// テンプレートUを利用して、args...それぞれから生成した値を並べたinteger_sequenceを返す
template<template<T> class U>
using value_map = integer_sequence<decltype(U<T{}>::value), U<args>::value ...>;

// テンプレートUを利用して、args...それぞれから生成した型を並べたtype_sequenceを返す
template<template<T> class U>
using type_map = type_sequence<typename U<args>::type ...>;

// args...を引数に取る関数を実行する
template<typename F>
static inline constexpr auto exec(F&& f) noexcept(noexcept(f(args...))) -> decltype(f(args...)) {
return f(args...);
}

// args...をコンストラクタに渡して、型Uのインスタンスを作成する
template<typename U>
static inline constexpr U create_instance() noexcept(noexcept(U{args...})) { return U{args...}; }

// args2... を末尾に追加して作成したinteger_sequenceを返す
template<T ...args2>
using append = integer_sequence<T, args..., args2...>;

// args2... を先頭に追加して作成したinteger_sequenceを返す
template<T ...args2>
using prepend = integer_sequence<T, args2..., args...>;

// 他のinteger_sequenceと結合して作成したinteger_sequenceを返す
template<typename U, enable_when<is_integer_sequence_t<T, U>{}> = nullptr>
using join = typename U::template prepend<args...>;

// args...のn番目の値を返す。
static inline constexpr T at(size_type n) noexcept {
return ((T[size()]){args...})[n];
}

// 配列っぽい何かのように扱うために
inline constexpr T operator[](size_type n) const noexcept { return at(n); }

// 先頭を返す
static inline constexpr T front() noexcept { return at(0); }

// 末尾を返す
static inline constexpr T back() noexcept { return at(size()-1); }

// n番目からm-1番めを切り取って作成したinteger_sequenceを返す
template<size_type n = 0, size_type m = size(), enable_when<(n<=m&&m<=size())> = nullptr>
using slice = integer_sequence<T, /*args...のn番目からm-n個*/>;

// 先頭からn個を切り取って作成したinteger_sequenceを返す
template<std::size_t n = 1>
using head = slice<0, n>;

// 末尾からn個を切り取って作成したinteger_sequenceを返す
template<size_type n = 1>
using tail = slice<size()-n>;

// n番目にargs2...を挿入して作成したinteger_sequenceを返す
template<size_type n, T ...args2>
using insert = typename head<n>::template append<args2...>::template join<tail<size()-n>>;

// n番目からc個を削除した残りで作成したinteger_sequenceを返す
template<size_type n, size_type c = 1>
using erase = head<n>::template join<tail<size()-n-c>>;

// args...を逆転して作成したinteger_sequenceを返す
using reverse = integer_sequence<T, /*args...を逆転したもの*/>;

// 最初にvalueが見つかる位置を探す
static inline constexpr size_type find(T value, size_type pos = 0) noexcept {
return (pos>=size) ? npos: ((at(pos)==value) ? pos : find(value, pos+1));
}
};

template<typename T>
struct for_each_;

template<typename T, T ...args>
struct for_each_<integer_sequence<T, args...>>
{
struct ignore {
inline ignore(...) {}
};
template<typename F>
static inline void call(F&& f) noexcept(noexcept(f(T{}))) {
ignore{(f(args),0)...};
}
};

// integer_sequence型の中身それぞれを引数にして関数fを実行する
template<typename T, typename F>
inline void for_each(F&& f) noexcept(noexcept(for_each_<T>::call(std::forward<F>(f)))) {
return for_each_<T>::call(std::forward<F>(f));
}

}


ほとんどのメンバについては説明の必要はないかと思いますが、一部やや複雑な部分を省略してあります。

詳細は以下で説明します。


make_integer_sequence


c++


template<typename T, T n, std::size_t c>
struct make_integer_sequence_
{
using front_half_ = typename make_integer_sequence_<T, n, c/2>::type;
using back_half_ = typename make_integer_sequence_<T, static_cast<T>(n+c/2), (c+1)/2>::type;
using type = typename front_half_::template join<back_half_>;
};

template<typename T, T n>
struct make_integer_sequence_<T, n, 1>
{
using type = integer_sequence<T, n>;
};

template<typename T, T n>
struct make_integer_sequence_<T, n, 0>
{
using type = integer_sequence<T>;
};

template<typename T, T n, std::size_t c>
using make_integer_sequence = typename make_integer_sequence_<T, n, c>::type;


std::make_integer_sequenceと同じようなものですが、こんな風に実装しています。std::make_integer_sequenceと違い、開始点と個数を与えるようにしてある分若干複雑になっていますが、恐らくそれほど違いはないと思われます。

std::make_integer_sequenceは再帰の回数がO(logn)で実装されることが仕様で明記されているので、これもO(logn)で実装するために線形再帰ではなく2分割して生成したものを結合しています。


slice


c++

    template<std::size_t ...indices>

struct slice_
{
using type = integer_sequence<T, at(indices)...>;
};

template<std::size_t n = 0, std::size_t m = size(), enable_when<(n<=m&&m<=size())> = nullptr>
using slice = typename make_integer_sequence<std::size_t, n, m-n>::template unpack<slice_>::type;


まず初めに、n...m-1のinteger_sequenceを作成してから、テンプレートクラスslice_に対してunpackを適用し、slice_<n, n+1, ..., m-1>を作成しています。

slice_の受け取ったパラメータパックを利用すると、integer_sequence<T, at(n), at(n+1), ..., at(m-1)>を得ることができます。

ちなみに、make_integer_sequenceで開始地点以外の範囲を生成するようにしたのは、ここで使うためでした。


reverse


c++

    static constexpr std::size_t reverse_(std::size_t n) noexcept { return size()-n-1; }

using reverse = typename make_index_sequence<size()>::template map<std::size_t, reverse_>::template unpack<slice_>::type;

make_index_sequenceinteger_sequence<std::size_t, 0, 1, 2, ..., size()-1>を作成した後、mapを利用してinteger_sequence<std::size_t, size()-1, size()-2, size()-3, ..., 1, 0>を得ています。

後はsliceの場合と同じようにできるので、slice_テンプレートクラスを再利用しています。


サンプルコード


c++

inline constexpr int func(int x) noexcept { return x*x; }

int main() {
using a = geranium::integer_sequence<int, 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5>;
using b = a::map<int, func>; // geranium::integer_sequence<int, 9, 1, 16, 1, 25, 81, 4, 36, 25, 9, 25>
using c = b::slice<1, b::size()-1>; // geranium::integer_sequence<int, 1, 16, 1, 25, 81, 4, 36, 25, 9>

auto f = [](int x){ std::cout << x << " "; };
geranium::for_each<a>(f);
std::cout << std::endl;
geranium::for_each<b>(f);
std::cout << std::endl;
geranium::for_each<c>(f);
std::cout << std::endl;
}



出力

3 1 4 1 5 9 2 6 5 3 5 

9 1 16 1 25 81 4 36 25 9 25
1 16 1 25 81 4 36 25 9


で、これ、何に使うの?

何に使えば良いのでしょうか。私自身良く分かっていません。

タプルを展開するとかならstd::integer_sequenceさえあれば大体事足りますし、静的な配列が欲しいならconstexprで用意すればいいような。

ぶっちゃけ、「できるからやってみたかった」だけです。

でも、こんなことが出来ると分かっていると、もしかしたら誰かが、ピンポイントで「ああ、こういう機能があったら」と思った時に使えるかも知れません。

そんな日は永遠に来ないかもしれませんが。

次回は今回と同じようなメタ配列の、型バージョンでも作ろうかと思います。

それでは本日はこの辺で