- N3915 apply() call a function with arguments from a tuple (V3)
- N4081 Working Draft, C++ Extensions for Library Fundamentals
レビュアー:高橋 晶(Akira Takahashi, faithandbrave@longgate.co.jp)
apply()
関数は、タプルを引数として関数を呼び出すユーティリティ関数です。しかしこの関数には、標準アルゴリズムと組み合わせて使用できないという問題があります。このレビューでは、apply()
のユースケースを整理し、いくつかの有用なケースでapply()
を適用できないことを示します。その後、その改善のために、関数オブジェクトをタプルを引数として受け取れるよう変換する機能と、その引数適用という2段階化の提案を行います。
// <experimental/tuple> header
namespace std {
namespace experimental {
inline namespace fundamentals_v1 {
template <class F, class Tuple>
constexpr decltype(auto) apply(F&& f, Tuple&& t);
}}}
apply()
は、引数としてタプルを与えると、タプルを展開して関数オブジェクトを呼び出すユーティリティ関数です。
void f(int, char, double);
apply(f, std::make_tuple(1, 'a', 3.14));
###ユースケース1 引数の転送
apply()
は、関数の引数を一旦、クラスのメンバ変数として保持しておき、あとで関数の引数としてそれを渡して呼び出す、というケースで使用します。
可変個メンバ変数というのが定義できないため、引数をメンバ変数に保持する場合、タプルを使用することになります。
class X {
std::tuple<int, char, double> args_;
public:
void setArgs(int a, char b, double c)
{
args_ = std::make_tuple(a, b, c);
}
template <class F>
void call(F f)
{
apply(f, args_);
}
};
map
の全要素に対して処理を行う場合、for_each()
アルゴリズムを使用して以下のようなコードを書くことになります。
std::map<int, std::string> m;
std::for_each(m.begin(), m.end(), [](const std::pair<const int, std::string>& x) {
// …x.first, x.secondを使ってキー、値にアクセスする…
});
apply()
は、アルゴリズムと組み合わせて使用することができず、このユースケースをサポートしていません。
ジェネリックラムダとapply()
を組み合わせればこのユースケースでも扱えますが、毎回このようなコードを書くのは苦痛です。
std::for_each(m.begin(), m.end(), [](const auto& args) { apply([](int key, const std::string& value) {
// …keyを使ってキーに、valueを使って値にアクセスする…
}, args); });
Boost.Rangeのcombine()
、またその前身となったHaskell言語のzip
関数は、複数のRangeを合成します。
たとえば、std::vector<T>
のRangeと、0
から始まるインデックスのRangeをcombine()
で綴じ合わせると、ZipRange<std::tuple<T, std::size_t>>
のようになり、for_each()
アルゴリズム内で現在のインデックスを取得できるようになります。
std::vector<T> v;
auto r = boost::combine(v, boost::irange(0u));
std::for_each(r.begin(), r.end(), [](std::tuple<T, std::size_t> args) {
T value = std::get<0>(args);
std::size_t index = std::get<1>(args);
});
このケースもまた、apply()
が有用ですが、ユースケース2と同様にapply()
を適用できません。
これに類するケースは、ほかに無数にあります:
- グラフの辺リストを走査する際、辺の2つの頂点を抽出する。
Graph g;
auto edge_range = edges(g);
std::for_each(begin(edge_range), end(edge_range), [](std::tuple<Vertex, Vertex> e) {
Vertex source = std::get<0>(e);
Vertex target = std::get<1>(e);
});
- 隣接する値の比較
std::vector<T> v = {1, 2, 3};
auto r = v | adjacent_zipped; // {(1, 2), (2, 3)}
std::for_each(r.begin(), r.end(), [](const std::tuple<T, T>& x) {
T a = std::get<0>(x);
T b = std::get<1>(x);
if (a < b) {
// …
}
});
この問題を解決し、apply()
をアルゴリズムと組み合わせられるようにするには、「複数引数を受け取る関数を、タプルを引数として実行する形に変換する」という機能とその引数適用を、2段階に分けて行える必要があります。
その実装は、apply()
関数のラッパーとして実装できます。
#include <tuple>
#include <utility>
template<typename F, typename Tuple, size_t... I>
auto apply_impl(F&& f, Tuple&& args, std::index_sequence<I...>)
{
return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...);
}
template<typename F, typename Tuple,
typename Indices = std::make_index_sequence<std::tuple_size<Tuple>::value>>
auto apply(F&& f, Tuple&& args)
{
return apply_impl(std::forward<F>(f), std::forward<Tuple>(args), Indices());
}
template<typename F, typename Tuple, size_t... I>
auto apply_impl(F&& f, const Tuple& args, std::index_sequence<I...>)
{
return std::forward<F>(f)(std::get<I>(args)...);
}
template<typename F, typename Tuple,
typename Indices = std::make_index_sequence<std::tuple_size<Tuple>::value>>
auto apply(F&& f, const Tuple& args)
{
return apply_impl(std::forward<F>(f), args, Indices());
}
template <typename F>
class apply_functor {
F f_;
public:
explicit apply_functor(F&& f)
: f_(std::forward<F>(f)) {}
template <typename Tuple>
auto operator()(Tuple&& args)
{
return apply(std::forward<F>(f_), std::forward<Tuple>(args));
}
template <typename Tuple>
auto operator()(const Tuple& args)
{
return apply(std::forward<F>(f_), args);
}
};
template <typename F>
apply_functor<F> make_apply(F&& f)
{
return apply_functor<F>(std::forward<F>(f));
}
使用例:
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
int main()
{
std::vector<std::tuple<int, char, std::string>> v = {
{1, 'a', "Alice"},
{2, 'b', "Bob"},
{3, 'c', "Carol"}
};
std::for_each(v.begin(), v.end(),
make_apply([](int a, char b, const std::string& c) {
std::cout << a << ' ' << b << ' ' << c << std::endl;
}
));
}
C++はその設計思想において、直交性を重視しています。すなわち、機能同士が、容易に組み合わせて使用できる必要があります。apply()
関数を標準化するのであれば、標準アルゴリズムと組み合わせて使用できるべきだと考えます。
この提案におけるapply()
とmake_apply()
の関係は、Boost Fusion Libraryですでに前例があります。Fusionライブラリでは、fused
とmake_fused()
という機能で、タプルを関数の引数として適用できるようにしています。
Boostは標準化における実験場としての役割も持っているので、この前例を調査する必要があるでしょう。
apply()
can't combine with algorithms
apply()
is useful for many use cases. However, apply()
is not support some use cases.
std::map<int, std::string> m;
std::for_each(m.begin(), m.end(), [](const std::pair<const int, std::string>& x) {
// key access with `x.first`, and value access with `x.second`...
});
If we use apply()
in this case, we need follow code:
std::for_each(m.begin(), m.end(), [](const auto& args) {
apply([](int key, const std::string& value) {
// key access with `key`, and value access with `value`...
},
args);
});
The code is very verbose.
Boost.Range's combine()
function and Haskell's zip
function combine multiple range.
For example, if we zip with std::vector<T>
and index range from 0
, we can get index in for_each()
algorithm.
std::vector<T> v;
auto r = boost::combine(v, boost::irange(0u));
std::for_each(r.begin(), r.end(), [](std::tuple<T, std::size_t> args) {
T value = std::get<0>(args);
std::size_t index = std::get<1>(args);
});
This case is useful, but apply()
can't combination with algorithm.
We need a translation function object to acceptable tuple as argument. We propose make_apply()
function.
#include <tuple>
#include <utility>
template<typename F, typename Tuple, size_t... I>
auto apply_impl(F&& f, Tuple&& args, std::index_sequence<I...>)
{
return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...);
}
template<typename F, typename Tuple,
typename Indices = std::make_index_sequence<std::tuple_size<Tuple>::value>>
auto apply(F&& f, Tuple&& args)
{
return apply_impl(std::forward<F>(f), std::forward<Tuple>(args), Indices());
}
template<typename F, typename Tuple, size_t... I>
auto apply_impl(F&& f, const Tuple& args, std::index_sequence<I...>)
{
return std::forward<F>(f)(std::get<I>(args)...);
}
template<typename F, typename Tuple,
typename Indices = std::make_index_sequence<std::tuple_size<Tuple>::value>>
auto apply(F&& f, const Tuple& args)
{
return apply_impl(std::forward<F>(f), args, Indices());
}
template <typename F>
class apply_functor {
F f_;
public:
explicit apply_functor(F&& f)
: f_(std::forward<F>(f)) {}
template <typename Tuple>
auto operator()(Tuple&& args)
{
return apply(std::forward<F>(f_), std::forward<Tuple>(args));
}
template <typename Tuple>
auto operator()(const Tuple& args)
{
return apply(std::forward<F>(f_), args);
}
};
template <typename F>
apply_functor<F> make_apply(F&& f)
{
return apply_functor<F>(std::forward<F>(f));
}
Usage:
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
int main()
{
std::vector<std::tuple<int, char, std::string>> v = {
{1, 'a', "Alice"},
{2, 'b', "Bob"},
{3, 'c', "Carol"}
};
std::for_each(v.begin(), v.end(),
make_apply([](int a, char b, const std::string& c) {
std::cout << a << ' ' << b << ' ' << c << std::endl;
}
));
}
Boost Fusion Library already has make_apply()
like function as boost::fusion::make_fused()
. Boost has an aspect as experimental standard. We should study from Boost's experience.