After watching Andrei Alexandrescu’s talk on Going Native 2013, I wanted to take a crack at it myself. The presentation covers how to expand a tuple into individual arguments in a function call. Being a Python programmer I’m a little spoiled by func(*args)
so the ability to do this in C++11 is something I’m eager to use. What I came up with wound up being quite similar, but more flexible. I wanted to make it more generic, to work with std::pair
and std::array
. The version presented in that video is incredibly powerful, but it can go a bit further.
The limitations start at the top level, the explode
free function.
template <class F, class... Ts>
auto explode(F&& f, const tuple<Ts...>& t)
-> typename result_of<F(Ts...)>::type
{
return Expander<sizeof...(Ts),
typename result_of<F(Ts...)>::type,
F,
const tuple<Ts...>&>::expand(f, t);
}
The tuple&
argument allows a means to use result_of
to figure out the return type, and sizeof...
to determine the size of the tuple
itself. This can be accomplished via other means. decltype
can be used to figure out the return type. It needs more typing, but removes the need for result_of
. As for sizeof...
, there is a std::tuple_size
available which can reach the same end. Using this makes explode
non-variadic. Taking a universal reference, rather than capturing the parameter pack, means different versions for lvalue and rvalue refs aren’t needed.
My initial function (called expand
instead) is:
template <typename Functor, typename Tup>
auto expand(Functor&& f, Tup&& tup)
-> decltype(Expander<std::tuple_size<typename std::remove_reference<Tup>::type>::value, Functor, Tup>::call(
std::forward<Functor>(f),
std::forward<Tup>(tup)))
{
return Expander<
std::tuple_size<typename std::remove_reference<Tup>::type>::value,
Functor,
Tup>::call(
std::forward<Functor>(f),
std::forward<Tup>(tup));
}
Some things to note:
-
std::tuple_size
works on std::pair
(yielding 2) and on std::array
(yielding the size of the array).
-
std::get
also supports std::pair
and std::array
, meaning that now tuple
, pair
, and array
can all work in this context.
-
std::remove_reference
is needed for calling std::tuple_size
because tup
is a universal reference, and Tup
may deduce to an lvalue reference type
The decltype
goes through each level of the expansion, until much like the original, it hits a base case and does the call.
#include <cstddef>
#include <tuple>
#include <utility>
#include <type_traits>
#include <array>
template <std::size_t Index, typename Functor, typename Tup>
struct Expander {
template <typename... Ts>
static auto call(Functor&& f, Tup&& tup, Ts&&... args)
-> decltype(Expander<Index-1, Functor, Tup>::call(
std::forward<Functor>(f),
std::forward<Tup>(tup),
std::get<Index-1>(tup),
std::forward<Ts>(args)...))
{
return Expander<Index-1, Functor, Tup>::call(
std::forward<Functor>(f),
std::forward<Tup>(tup),
std::get<Index-1>(tup),
std::forward<Ts>(args)...);
}
};
template <typename Functor, typename Tup>
struct Expander<0, Functor, Tup> {
template <typename... Ts>
static auto call(Functor&& f, Tup&&, Ts&&... args)
-> decltype(f(std::forward<Ts>(args)...))
{
static_assert(
std::tuple_size<
typename std::remove_reference<Tup>::type>::value
== sizeof...(Ts),
"tuple has not been fully expanded");
// actually call the function
return f(std::forward<Ts>(args)...);
}
};
template <typename Functor, typename Tup>
auto expand(Functor&& f, Tup&& tup)
-> decltype(Expander<std::tuple_size<
typename std::remove_reference<Tup>::type>::value,
Functor,
Tup>::call(
std::forward<Functor>(f),
std::forward<Tup>(tup)))
{
return Expander<std::tuple_size<
typename std::remove_reference<Tup>::type>::value,
Functor,
Tup>::call(
std::forward<Functor>(f),
std::forward<Tup>(tup));
}
A few examples showing the flexibility.
int f(int, double, char);
int g(const char *, int);
int h(int, int, int);
int main() {
expand(f, std::make_tuple(2, 2.0, '2'));
// works with pairs
auto p = std::make_pair("hey", 1);
expand(g, p);
// works with std::arrays
std::array<int, 3> arr = {{1,2,3}};
expand(h, arr);
}
Each level of the call takes one argument at a time off the back of the tuple
using std::get
and the template Index parameter, decrements the index, and recurses. This is a bit hard to imagine, so I’ll illustrate. This sequence is not meant to be taken too literally.
Let’s say I have a tuple
of string
, int
, char
, and double
. I’ll denote this example tuple as tuple("hello", 3, 'c', 2.0)
. The expansion would happen something like the following
expand(f, tuple("hello", 3, 'c', 2.0))
-> call<4>(f, tuple("hello", 3, 'c', 2.0))
-> call<3>(f, tuple("hello", 3, 'c', 2.0), 2.0)
-> call<2>(f, tuple("hello", 3, 'c', 2.0), 'c', 2.0)
-> call<1>(f, tuple("hello", 3, 'c', 2.0), 3, 'c', 2.0)
-> call<0>(f, tuple("hello", 3, 'c', 2.0), "hello", 3, 'c', 2.0)
-> f("hello", 3, 'c', 2.0)
Of course std::integer_sequence
in C++14 turns all of this on its head. Maybe I should’ve implemented that instead…