Background/Motivation
I've been playing around with VC++2015, looking at some ways of writing utility routines to handle tuples and other variadic bits and pieces.
My first function of interest is the common-or-garden tuple_for_all function. For a function f
and a tuple t
call in turn f(get<0>(t)
, f(get<1>(t)
, and so on.
So far, so 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, that works (modulo any compilation and copy/paste errors that occurred in cutting this down).
But my next thought was that the Function
might return a useful/interesting value in some cases, and so we should capture it. Naively, we could do something like 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 for when the functions do return a value, but since void
is annoyingly degenerate and we can't make a std::tuple<void>
, it won't work for void
-returning functions. We can't directly overload by return type, but C++ gives us the tools handle this, with 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 is nice enough. It'd be nice if the evaluation order were consistent between the two (the void
version is left-to-right, the value-returning version is up to the compiler, so probably right-to-left). We can fix that by eschewing the call to std::make_tuple
and instead brace initializing a std::tuple
instead. I don't know if there's something better than the decltype(std::make_tuple(...))
to get the right type to 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 to be bugged right now; it still doesn't use left-to-right evaluation even for the braced initializer because the optimizer team doesn't seem to think it's important)
I'm more interested in the std::enable_if_t
check. We do not examine to see if the function returns non-void
for every type in the tuple, only the first. But really, it should be all-or-nothing. Columbo's all_true
technique takes care of that for 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 the bit that's tricky. While tuple_for_each
is nice and useful, I thought, what if I spiced it up a little? How about tuple_for_each
that takes a function f
and tuples t0
, t1
, etc. and computes f(get<0>(t0), get<0>(t1)...)
, f(get<1>(t0), get<1>(t1)...)
and so on?
Naively we'd like to do something like 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 like the first ...
to expand Tuples
, and the second ...
to expand Indices
. But parameter pack expansions don't offer this kind of control. If the expression before the ...
contains multiple parameter packs, then the ...
tries to unpack all of them in parallel (VC++; it emits a compiler error that they are different lengths), or cannot find the parameter packs at all (g++; it emits a compiler error that there are no packs).
Fortunately, this case can be handled with an additional layer of indirection to separate out the 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... those pesky enable_ifs. I had to weaken them, back to testing only the first elements in the tuples. Now, it's not a complete disaster, because the inner-most expansion can perform the check. But it's not great. Consider the following:
struct functor
{
int operator()(int a, int b) { return a + b; }
double operator()(double a, double b) { return a + 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 to force the use of the void
path, because evaluating function on the third tuple element returns void
. But our enable check only looks at the first element. And because the failure happens after SFINAE-driven "overload" resolution, SFINAE can't save us here.
But equally, we can't double-unpack the enable_if_t
expression for the same reason that we couldn't do it when invoking the function: the parameter pack expansion gets confused and tries to iterate both at the same time.
And this is where I come unstuck. I need an indirection morally equivalent to the one used to call the function, but I can't immediately see how to write that indirection such that it actually works.
Any suggestions?