最近C++に再入門しています。
先日C++14の範囲で「タプルを展開して関数の引数として渡す」関数が必要になったので、C++17で標準ライブラリとして追加された std::apply
関数の実装を読みつつ、ほんの少し改変してC++14でも実行できるように再実装してみました。
この記事は std::apply
関数の説明と内部でどのような処理を行っているか1行1行、自分が理解している範囲で解説する記事になります。
std::apply
std::apply
は「タプル型を展開して関数の引数として渡す」という処理を行う関数です1。
以下は tuple<int, int, int>
を展開し、関数 add(int, int, int)
の引数として処理を実行しています。
# include <cstdio>
# include <tuple>
void add(int a, int b, int c) {
printf("total = %d\n", a + b + c);
}
int main() {
auto tpl = std::make_tuple(1, 2, 3); // make_tupleから {1, 2, 3} を持つtuple<int, int, int>を構築。
std::apply(add, tpl); /// total = 6
}
標準ライブラリの実装
次に実際の標準ライブラリの実装を読んでみます。実行環境は gcc 9.2.1
です。
std::apply
std::apply
は実行する関数とタプルを引数として取り、std::__apply_impl
を呼び出します。
以下が std::apply
の実装です。
template <typename _Fn, typename _Tuple>
constexpr decltype(auto)
apply(_Fn&& __f, _Tuple&& __t)
{
using _Indices
= make_index_sequence<tuple_size_v<remove_reference_t<_Tuple>>>;
return std::__apply_impl(std::forward<_Fn>(__f),
std::forward<_Tuple>(__t),
_Indices{});
}
まず関数のシグネチャ部分に注目します。
template <typename _Fn, typename _Tuple>
constexpr decltype(auto)
apply(_Fn&& __f, _Tuple&& __t)
{ /* ... */ }
std::apply
は引数を2つ取り、返り値は decltype(auto)
2 になっています。
これは std::apply
の返り値は _Fn
に依存するためであり、正確に返り値の型を推論するのに必要です。
次に実装を見ていきます。
using _Indices
= make_index_sequence<tuple_size_v<remove_reference_t<_Tuple>>>;
std::make_index_sequence
はテンプレートの引数で要素数を指定すると、テンプレート内に 0...要素数-1
の整数列を構築してくれます。後にこのとき構築した整数列を使用してタプルを展開します。
std::make_index_sequence
のテンプレート引数の中身ですが、remove_reference_t
で参照を取り外したタプルのサイズを tuple_size_v
で取得しています3。
tuple_size_v
は tuple_size<_Tp>::value
のエイリアスであり、変数テンプレートが導入されたC++14から使用可能です。
最後に返り値です。
return std::__apply_impl(std::forward<_Fn>(__f),
std::forward<_Tuple>(__t),
_Indices{});
std::apply
の引数2つと手前で定義した整数列をテンプレートとして持つ _Indices
のインスタンスを引数として std::__apply_impl
を実行しています。
std::forward
は引数の型をそのままに次の関数へと転送します。手前2つの引数の型はユニバーサル参照ですので lvalue
でも rvalue
の可能性もあります。従って std::forward
を使わなければ元の型のまま転送できません。
最後に _Indice{}
ですが、ここでは _Indice
の持つテンプレート内の整数列が必要です。
そのため、ここではとりあえずインスタンスを作っていますが、__apply_impl
では無名の引数になっています。
std::__apply_impl
std::__apply_impl
は引数を3つ取り、タプルを展開して std::__invoke
に処理を引き継ぎます。
以下実装です。実際の処理は1行しかありません。
template <typename _Fn, typename _Tuple, size_t... _Idx>
constexpr decltype(auto)
__apply_impl(_Fn&& __f, _Tuple&& __t, index_sequence<_Idx...>)
{
return std::__invoke(std::forward<_Fn>(__f),
std::get<_Idx>(std::forward<_Tuple>(__t))...);
}
テンプレート内の size_t... _Idx
は _Indices
内の整数列が入っています。std::__apply_impl
はこの可変長テンプレート引数を使うことでタプルを1つずつ展開していきます。
該当部分はこの関数唯一の処理である、戻り値部分にあります。
return std::__invoke(std::forward<_Fn>(__f),
std::get<_Idx>(std::forward<_Tuple>(__t))...);
std::get<Idx>
はテンプレートの引数に当たるタプルの内容を取り出します。_Idx
は前述の通り整数ですが、可変長引数のためそのままは使えません。これを展開して一整数として使うには ...
でパラメータパックを展開する必要があります。
ここでは、std::get
の末尾にくっついていますね。これによってタプルの中身が展開され、std::__invoke
の引数として渡されることになりました。
ここまでで apply
関連の実装は終了です(あくまで自分はそう思っています)。
この先は invoke
がうまくやってくれます。そして関数が実行されます。
apply (C++14 ver)
std::__apply_impl
で関数本体を実行してくれていた invoke
は C++17 で追加された機能です4。
そのためこの部分だけを置き換えます。apply
は実装が同じなので省略しました。
template <class Fn, class Tuple, size_t... _Idx>
constexpr decltype(auto)
apply_impl(Fn&& f, Tuple&& t, std::index_sequence<_Idx...>) {
return f(std::get<_Idx>(std::forward<Tuple>(t))...);
}
といっても、関数 f
にタプルの中身を展開してそのまま実行しているだけです。
これで疑似applyが完成しました。
(とりあえず動くだけではありますが...)
標準ライブラリ読むのは楽しいですね。 今後もどんどん読める範囲から参考にしていきたい。