c++ - Controlling the unpacking of multiple variadic parameter packs for a fancier tuple_for_each -
background/motivation
i've been playing around vc++2015, looking @ ways of writing utility routines handle tuples , other variadic bits , pieces.
my first function of interest common-or-garden tuple_for_all function. function f
, tuple t
call in turn f(get<0>(t)
, f(get<1>(t)
, , on.
so far, straightforward.
template<typename tuple, typename function, std::size_t... indices> constexpr void tuple_for_each_aux(function&& f, tuple&& t, std::index_sequence<indices...>) { using swallow = int[]; static_cast<void>(swallow{ 0, (std::forward<function>(f)(std::get<indices>(std::forward<tuple>(t))), void(), 0)... }); } template<typename function, typename tuple> constexpr void tuple_for_each(function&& f, tuple&& t) { return tuple_for_each_aux(std::forward<function>(f), std::forward<tuple>(t), std::make_index_sequence<std::tuple_size<std::decay_t<tuple>>::value>{}); }
ok, works (modulo compilation , copy/paste errors occurred in cutting down).
but next thought function
might return useful/interesting value in cases, , should capture it. naively, this:
template<typename tuple, typename function, std::size_t... indices> constexpr auto tuple_for_each_aux(function&& f, tuple&& t, std::index_sequence<indices...>) { return std::make_tuple(std::forward<function>(f)(std::get<indices>(std::forward<tuple>(t))); } template<typename function, typename tuple> constexpr auto tuple_for_each(function&& f, tuple&& t) { return tuple_for_each_aux(std::forward<function>(f), std::forward<tuple>(t), std::make_index_sequence<std::tuple_size<std::decay_t<tuple>>::value>{}); }
that's great when functions return value, since void
annoyingly degenerate , can't make std::tuple<void>
, won't work void
-returning functions. can't directly overload return type, c++ gives tools handle this, sfinae:
template<typename function, typename tuple, std::size_t... indices, typename = std::enable_if_t<std::is_void<std::result_of_t<function(std::tuple_element_t<0, std::decay_t<tuple>>)>>::value>> constexpr void tuple_for_each_aux(function&& f, tuple&& t, std::index_sequence<indices...>) { using swallow = int[]; static_cast<void>(swallow{ 0, (std::forward<function>(f)(std::get<indices>(std::forward<tuple>(t))), void(), 0)... }); } template<typename function, typename tuple, std::size_t... indices, typename = std::enable_if_t<!std::is_void<std::result_of_t<function(std::tuple_element_t<0, std::decay_t<tuple>>)>>::value>> constexpr decltype(auto) tuple_for_each_aux(function&& f, tuple&& t, std::index_sequence<indices...>) { return std::make_tuple(std::forward<function>(f)(std::get<indices>(std::forward<tuple>(t)))...); } template<typename function, typename tuple> constexpr decltype(auto) tuple_for_each(function&& f, tuple&& t) { return tuple_for_each_aux(std::forward<function>(f), std::forward<tuple>(t), std::make_index_sequence<std::tuple_size<std::decay_t<tuple>>::value>{}); }
this nice enough. it'd nice if evaluation order consistent between 2 (the void
version left-to-right, value-returning version compiler, right-to-left). can fix eschewing call std::make_tuple
, instead brace initializing std::tuple
instead. don't know if there's better decltype(std::make_tuple(...))
right type construct. there may be.
template<typename function, typename tuple, std::size_t... indices, typename = std::enable_if_t<std::is_void<std::result_of_t<function(std::tuple_element_t<0, std::decay_t<tuple>>)>>::value>> constexpr void tuple_for_each_aux(function&& f, tuple&& t, std::index_sequence<indices...>) { using swallow = int[]; static_cast<void>(swallow{ 0, (std::forward<function>(f)(std::get<indices>(std::forward<tuple>(t))), void(), 0)... }); } template<typename function, typename tuple, std::size_t... indices, typename = std::enable_if_t<!std::is_void<std::result_of_t<function(std::tuple_element_t<0, std::decay_t<tuple>>)>>::value>> constexpr decltype(auto) tuple_for_each_aux(function&& f, tuple&& t, std::index_sequence<indices...>) { return decltype(std::make_tuple(std::forward<function>(f)(std::get<indices>(std::forward<tuple>(t)))...)){std::forward<function>(f)(std::get<indices>(std::forward<tuple>(t))) ...}; } template<typename tuple, typename function> constexpr decltype(auto) tuple_for_each(function&& f, tuple&& t) { return tuple_for_each_aux(std::forward<function>(f), std::forward<tuple>(t), std::make_index_sequence<std::tuple_size<std::decay_t<tuple>>::value>{}); }
(incidentally, vc++ 2015 appears bugged right now; still doesn't use left-to-right evaluation braced initializer because optimizer team doesn't seem think it's important)
i'm more interested in std::enable_if_t
check. not examine see if function returns non-void
every type in tuple, first. really, should all-or-nothing. columbo's all_true
technique takes care of us:
template <bool...> struct bool_pack; template <bool... v> using all_true = std::is_same<bool_pack<true, v...>, bool_pack<v..., true>>; template<typename function, typename tuple, std::size_t... indices, typename = std::enable_if_t<all_true<std::is_void<std::result_of_t<function(std::tuple_element_t<indices, std::decay_t<tuple>>)>>::value...>::value>> constexpr void tuple_for_each_aux(function&& f, tuple&& t, std::index_sequence<indices...>) { using swallow = int[]; static_cast<void>(swallow{ 0, (std::forward<function>(f)(std::get<indices>(std::forward<tuple>(t))), void(), 0)... }); } template<typename function, typename tuple, std::size_t... indices, typename = std::enable_if_t<!std::is_void<std::result_of_t<function(std::tuple_element_t<0, std::decay_t<tuple>>)>>::value>> constexpr decltype(auto) tuple_for_each_aux(function&& f, tuple&& t, std::index_sequence<indices...>) { return decltype(std::make_tuple(std::forward<function>(f)(std::get<indices>(std::forward<tuple>(t)))...)){std::forward<function>(f)(std::get<indices>(std::forward<tuple>(t))) ...}; } template<typename function, typename tuple> constexpr decltype(auto) tuple_for_each(function&& f, tuple&& t) { return tuple_for_each_aux(std::forward<function>(f), std::forward<tuple>(t), std::make_index_sequence<std::tuple_size<std::decay_t<tuple>>::value>{}); }
problem
but here's bit that's tricky. while tuple_for_each
nice , useful, thought, if spiced little? how tuple_for_each
takes function f
, tuples t0
, t1
, etc. , computes f(get<0>(t0), get<0>(t1)...)
, f(get<1>(t0), get<1>(t1)...)
, on?
naively we'd this:
using swallow = int[]; static_cast<void>(swallow{ 0, ((std::forward<function>(f)(std::get<indices>(std::forward<tuples>(ts))...)), void(), 0)... });
naively, we'd first ...
expand tuples
, , second ...
expand indices
. parameter pack expansions don't offer kind of control. if expression before ...
contains multiple parameter packs, ...
tries unpack of them in parallel (vc++; emits compiler error different lengths), or cannot find parameter packs @ (g++; emits compiler error there no packs).
fortunately, case can handled additional layer of indirection separate out expansions:
template <bool...> struct bool_pack; template <bool... v> using all_true = std::is_same<bool_pack<true, v...>, bool_pack<v..., true>>; template<size_t n, typename function, typename... tuples, typename = std::enable_if_t<std::is_void<std::result_of_t<function(std::tuple_element_t<n, std::decay_t<tuples>>...)>>::value>> constexpr void tuple_for_each_aux(function&& f, tuples&&... ts) { return std::forward<function>(f)(std::get<n>(std::forward<tuples>(ts))...); } template<typename function, typename... tuples, std::size_t... indices, typename = std::enable_if_t<all_true<std::is_void<std::result_of_t<function(std::tuple_element_t<0, std::decay_t<tuples>>...)>>::value>::value>> constexpr void tuple_for_each_aux(function&& f, std::index_sequence<indices...>, tuples&&... ts) { using swallow = int[]; static_cast<void>(swallow{ 0, (tuple_for_each_aux<indices>(std::forward<function>(f), std::forward<tuples>(ts)...), void(), 0)... }); } template<std::size_t n, typename function, typename... tuples, typename = std::enable_if_t<!std::is_void<std::result_of_t<function(std::tuple_element_t<n, std::decay_t<tuples>>...)>>::value>> constexpr decltype(auto) tuple_for_each_aux(function&& f, tuples&&... ts) { return std::forward<function>(f)(std::get<n>(std::forward<tuples>(ts))...); } template<typename function, typename... tuples, std::size_t... indices, typename = std::enable_if_t<all_true<!std::is_void<std::result_of_t<function(std::tuple_element_t<0, std::decay_t<tuples>>...)>>::value>::value>> constexpr decltype(auto) tuple_for_each_aux(function&& f, std::index_sequence<indices...>, tuples&&... ts) { return decltype(std::make_tuple(tuple_for_each_aux<indices>(std::forward<function>(f), std::forward<tuples>(ts)...)...)) { tuple_for_each_aux<indices>(std::forward<function>(f), std::forward<tuples>(ts)...)... }; } template<typename function, typename tuple, typename... tuples> constexpr decltype(auto) tuple_for_each(function&& f, tuple&& t, tuples&&... ts) { return tuple_for_each_aux(std::forward<function>(f), std::make_index_sequence<std::tuple_size<std::decay_t<tuple>>::value>{}, std::forward<tuple>(t), std::forward<tuples>(ts)...); }
that works fine... except... pesky enable_ifs. had weaken them, testing first elements in tuples. now, it's not complete disaster, because inner-most expansion can perform check. it's not great. consider following:
struct functor { int operator()(int a, int b) { return + b; } double operator()(double a, double b) { return + b; } void operator()(char, char) { return; } }; int main() { auto t1 = std::make_tuple(1, 2.0, 'a'); auto t2 = std::make_tuple(2, 4.0, 'b'); tuple_for_each(functor{}, t1, t2); return 0; }
the functor
object needs force use of void
path, because evaluating function on third tuple element returns void
. our enable check looks @ first element. , because failure happens after sfinae-driven "overload" resolution, sfinae can't save here.
but equally, can't double-unpack enable_if_t
expression same reason couldn't when invoking function: parameter pack expansion gets confused , tries iterate both @ same time.
and come unstuck. need indirection morally equivalent 1 used call function, can't see how write indirection such works.
any suggestions?
a holder of types:
template<class...> class typelist {};
an alias template compute result of applying f
i
-th element of each tuple in tuples
:
template<class f, std::size_t i, class...tuples> using apply_result_type = decltype(std::declval<f>()(std::get<i>(std::declval<tuples>())...));
now compute list of result types:
template<class f, std::size_t...is, class... tuples> typelist<apply_result_type<f, is, tuples...>...> compute_result_types(typelist<f, tuples...>, std::index_sequence<is...>); template<class f, std::size_t size, class... tuples> using result_types = decltype(compute_result_types(typelist<f, tuples...>(), std::make_index_sequence<size>()));
and check typelist has no void
in it:
template<class... ts> all_true<!std::is_void<ts>::value...> do_is_none_void(typelist<ts...>); template<class tl> using has_no_void_in_list = decltype(do_is_none_void(tl()));
and actual sfinae (just showing one):
template<typename function, typename... tuples, std::size_t... indices, typename = std::enable_if_t<!has_no_void_in_list<result_types<function, sizeof...(indices), tuples...>>{}>> constexpr void tuple_for_each_aux(function&& f, std::index_sequence<indices...>, tuples&&... ts) { using swallow = int[]; static_cast<void>(swallow{ 0, (tuple_for_each_aux<indices>(std::forward<function>(f), std::forward<tuples>(ts)...), void(), 0)... }); }
demo.
Comments
Post a Comment