The C++ standard says "tuples are heterogeneous, fixed-size collections of values" - [tuple.general]. Collections generally have iterator types associated with them, but that's a bit of a challenge since the iterator model in C++ assumes that for a collection, the type of *(Collection<T>::iterator) is T. But if the collection isn't on T, but on Types…, you doesn't quite work to say *(Collection<typename… Types>) is of type …Types. You need something to hold that. But in many cases, std::variant can work. It doesn't quiet work, since we'd really need a variant of references to the elements of the tuple, so that they could be written to. However, for many purposes we can come close. For the case I was looking at, making copies is perfectly fine. What I'm looking for is something roughly with the signature
template <typename... Types
auto getElement(size_t i, std::tuple<Types...> tuple) -> std::variant<Types...>;
That is, something that will get me the i
th element of a tuple, as a variant with the same typelist as the tuple, with the index determined at runtime. All of the normal accessors are compile time. So need to do something that will make the compile time information available at runtime.
Start with something I do know how to do, idiomatically printing a tuple.
template <typename Func, typename Tuple, std::size_t... I>
void tuple_for_each_impl(Tuple&& tuple, Func&& f, std::index_sequence<I...>)
{
auto swallow = {0,
(std::forward<Func>(f)(
I, std::get<I>(std::forward<Tuple>(tuple))))...};
(void)swallow;
}
template <typename Func, typename... Args>
void tuple_for_each(std::tuple<Args...> const& tuple, Func&& f)
{
tuple_for_each_impl(tuple, f, std::index_sequence_for<Args...>{});
}
template <typename... Args>
void print(std::ostream& os, std::tuple<Args...> const& tuple)
{
auto printer = [&os](auto i, auto el) {
os << (i == 0 ? "" : ", ") << el;
return 0;
};
return tuple_for_each(tuple, printer);
}
Actually, a bit more complicated than the totally standard idiom, since it factors out the printer into a application across the tuple, but it's not much more compilcated. The tuple_for_each constructs an index sequence based on the argument list, and delegates that to the impl, which uses it to apply the function to each element of the tuple. The _impl ought to be in a nested detail namespace, so as not to leak out. Swallow is the typical name for using an otherwise unnamed, and uninteresting, type to apply something to each element of the tuple for a side-effect. The void cast is to make sure the variable is used, and is evaluated.
The next step is, instead of an application of a function for its side-effect, instead a mapping of the tuple, returning the transformed tuple.
template <typename Func, typename Tuple, std::size_t... I>
auto tuple_transform_impl(Tuple&& tuple, Func&& f, std::index_sequence<I...>)
{
return std::make_tuple(
std::forward<Func>(f)(std::get<I>(std::forward<Tuple>(tuple)))...);
}
template <typename Func, typename... Args>
auto tuple_transform(std::tuple<Args...>&& tuple, Func&& f)
{
return tuple_transform_impl(tuple, f, std::index_sequence_for<Args...>{});
}
template <typename Func, typename... Args>
auto tuple_transform(std::tuple<Args...> const& tuple, Func&& f)
{
return tuple_transform_impl(tuple, f, std::index_sequence_for<Args...>{});
}
Because the std::tuple is not a template parameter, I have to supply a const& and a forwarding-reference form to cover both cases. And I'm ignoring volatile quals. The _impl function uses forwarding-reference parameters, which will decay or forward properly using std::forward. Using it is straightforward.
std::tuple<int, double, long> t = std::make_tuple(1, 2.3, 1l);
auto transform = tupleutil::tuple_transform(t,
[](auto i) { return i + 1; });
EXPECT_EQ(3.3, std::get<1>(transform));
auto t2 = tupleutil::tuple_transform(std::make_tuple(4, 5.0),
[](auto i) { return i + 1; });
EXPECT_EQ(6, std::get<1>(t2));
So, for functions over all the types in a tuple, tuple is a Functor. That is, we can apply the function to all elements in the tuple, and it's just like making a tuple out of applying the functions to elements before making the tuple. If this sounds like a trivial distinction, you are mostly right. Almost all container-ish things are Functors, and a few non-containerish things are also. Plus Functor sounds more impressive.
The transform also suggests a way of solving the problem I was originally looking at. An array of the elements of the tuple each as a variant will let me permute them with std tools.
template <typename... Args, std::size_t... I>
constexpr std::array<std::variant<Args...>, sizeof...(Args)>
tuple_to_array_impl(std::tuple<Args...> const& tuple,
std::index_sequence<I...>)
{
using V = std::variant<Args...>;
std::array<V, sizeof...(Args)> array = {
{V(std::in_place_index_t<I>{}, std::get<I>(tuple))...}};
return array;
}
template <typename... Args>
constexpr std::array<std::variant<Args...>, sizeof...(Args)>
tuple_to_array(std::tuple<Args...> const& tuple)
{
return tuple_to_array_impl(tuple, std::index_sequence_for<Args...>{});
}
And that can be used something like:
TEST(TupleTest, to_array)
{
constexpr std::tuple<int, double, long> t = std::make_tuple(1, 2.3, 1l);
auto arr = tupleutil::tuple_to_array(t);
int i = std::get<int>(arr[0]);
EXPECT_EQ(1, i);
}
TEST(TupleTest, to_array_repeated)
{
constexpr std::tuple<int, int, int> t = std::make_tuple(1, 2, 3);
auto arr = tupleutil::tuple_to_array(t);
int i = std::get<2>(arr[2]);
EXPECT_EQ(3, i);
}
The second test is there because I was about to write, "as you can see, we can tell the differece between variants holding the same type", except that wasn't true. The original version of to_ar
constexpr std::tuple<int, double, long> t = std::make_tuple(1, 2.3, 1l);
std::variant<int, double, long> v0{1};
auto v = tupleutil::get(0, t);
EXPECT_EQ(v0, v);
ray didn't use the constructor form with std::in_place_index_t. The code I ended up with did, but not at this point. There's nothing like writing out what something is supposed to do to make you look and keep you honest.
So here, we're constructing an array of std::variant<Args…> and constructing each member with the argument pack expansion into the std::variant constructor using the I
th index value to get that element of the tuple, and recording that we're constructing the i
th alternative of the variant. The second test checks that. The 2nd element of the array must be the 2nd variant of the tuple, and can be retrieved only by std::get<2>().
This would allow me to permutate the elements of a tuple, but I'm fairly close now to being able to writing a version that allows choice of the element at runtime, rather than at compile time.
constexpr std::tuple<int, double, long> t = std::make_tuple(1, 2.3, 1l);
std::variant<int, double, long> v0{1};
auto v = tupleutil::get(0, t);
EXPECT_EQ(v0, v);
What I'm going to do is construct an array of the getters for the tuple, each of which will return the element wrapped in a variant. The signature of the array will be of function pointer type, because, quite conveniently, a non-capturing lambda can decay to a function pointer.
First getting the array of getters for the tuple
template <typename V, typename T, size_t I> auto get_getter()
{
return [](T const& t) {
return V{std::in_place_index_t<I>{}, std::get<I>(t)};
};
}
template <typename... Args, std::size_t... I>
auto tuple_getters_impl(std::index_sequence<I...>)
{
using V = std::variant<Args...>;
using T = std::tuple<Args...>;
using F = V (*)(T const&);
std::array<F, sizeof...(Args)> array
= {{get_getter<V, T, I>()...}};
return array;
}
template <typename... Args> auto tuple_getters(std::tuple<Args...>)
{
return tuple_getters_impl<Args...>(std::index_sequence_for<Args...>{});
}
So first a function that returns a function that constructs a variant around the value of what's returned from std::get<I>. Well, it could return anything that happens to have a constructor that takes a an in_place_index_t, take as the thing to be converted something that std::get<I> can extract from. This is actually a separate function because GCC was unhappy doing the template parameter pack expansion inline in the _impl function. Clang was happy with the expansion noted in the comment. I really have no idea who is wrong here, and the workaround was straight forward. The array is one of function pointers, which the returned lambdas can decay to.
Now the only remaining trick is to use this array as a table to dispatch to the appropriate getter for the tuple.
const auto get = [](size_t i, auto t) {
static auto tbl = tupleutil::tuple_getters(t);
return tbl[i](t);
};
Get the array as a static, so we only need to computer it once, and simply return
tbl[i](t)
TEST(TupleTest, gettersStatic)
{
constexpr std::tuple<int, double, long> t = std::make_tuple(1, 2.3, 1l);
std::variant<int, double, long> v0{1};
auto v = tupleutil::get(0, t);
EXPECT_EQ(v0, v);
int i = std::get<0>(v);
EXPECT_EQ(1, i);
auto v2 = tupleutil::get(1, t);
EXPECT_EQ(1ul, v2.index());
double d = std::get<double>(v2);
EXPECT_EQ(2.3, d);
constexpr auto t2 = std::make_tuple(2.4, 1l);
auto v3 = tupleutil::get(0, t2);
double d2 = std::get<double>(v3);
EXPECT_EQ(2.4, d2);
}