| RSS | GitHub |
Variadic visitation selects an element from a parameter pack using a run-time index, unlike parameter pack indexing that uses a compile-time index.
C++26 parameter pack indexing only works with compile-time indices, and for a good reason. Consider a function that returns the Nth element of a parameter pack
auto pack_at(integral_constant<size_t, N> index, auto... args)
{
return args...[N];
}The parameter pack may contain different types, and as C++ is statically typed the return type must be deduced at compile-time. Therefore, the index must be known at compile-time.
Our goal is to use a run-time index instead
auto variadic_at(size_t index, auto... args)
{
return args...[index]; // Fails... for now
}The code below assumes C++17 to use standard library facilities, such as
std::variant or std::apply, but if you have alternative implementations
of these, then the code can be back-ported to C++14, and even C++11 by
adding the appropriate trailing return types.
The variadic_at function could be changed to return std::variant
auto variadic_at(size_t index, auto... args) -> variant<decltype(args)...>;The value inside the variant is then accessed using std::visit
auto r = variadic_at(index, args...);
std::visit([] (auto&& v) { cout << v; }, r);Since we end up using a visitor anyways, we might as well start by building a variadic visitor solution.
Getting variadic visitation to work involves a number of tricks, and this is the most complicated part of this article so the implementation is deferred until the end. Once we have this building-block in place, everything else becomes simple.
The variadic visitation is declared as
template <typename Visitor, typename... Args>
constexpr auto variadic_visit(Visitor&&, size_t index, Args&&...);and used like this
variadic_visit(f, 0, a, b, c); // Invokes f(a)
variadic_visit(f, 1, a, b, c); // Invokes f(b)
variadic_visit(f, 2, a, b, c); // Invokes f(c)
variadic_visit(f, 3, a, b, c); // Error: out-of-boundsThis example uses literal indices to demonstrate how variadic visitation works, but it can be used with index variables as well.
Tuple visitation can be build on top of variadic visitation.
The trick is to use std::apply to expand the tuple elements into a parameter pack.
The variadic_visit function takes a visitor, an index, and a parameter pack,
but std::apply only supplies the parameter pack so we need to capture the visitor
and index.
We will use a lambda function that captures the visitor as a tuple to preserve
perfect forwarding.
template <typename Visitor, type-like Tuple>
constexpr auto tuple_visit(Visitor&& v, size_t index, Tuple&& t)
{
return apply([v = forward_as_tuple(v), index] (auto&&... args) {
return variadic_visit(forward<Visitor>(get<0>(v)),
index,
forward<decltype(args)>(args)...);
},
forward<Tuple>(t));
}Usage
auto t = make_tuple(a, b, c);
tuple_visit(f, 0, t); // Invokes f(a)
tuple_visit(f, 1, t); // Invokes f(b)
tuple_visit(f, 2, t); // Invokes f(c)
tuple_visit(f, 3, t); // Error: out-of-boundsNow we are able to create a function that returns a variant. The idea is to use a visitor that creates objects, and for that we use a slightly altered implementation of the P3841 proposal.
template <typename T>
struct constructor
{
template <typename... Args>
constexpr T operator()(Args&&... args)
noexcept(is_nothrow_constructible_v<T, Args...>)
{
return T(forward<Args>(args)...);
};
};The variant type is deduced from the argument types.
template <typename... Args>
constexpr auto variadic_at(size_t index, Args&&... args)
{
using result_type = variant<Args...>;
return variadic_visit(constructor<result_type>{},
index,
forward<Args>(args)...);
}It would be a good idea to remove duplicated types from the variant, but we have skipped over that for brevity.
The same can be done for tuples, where the tuple-like type must be transformed into a variant type.
template <tuple-like Tuple>
constexpr auto tuple_at(size_t index, Tuple&& t)
{
using result_type = deduce_variant_t<Tuple>;
return tuple_visit(constructor<result_type>{},
index,
forward<Tuple>(t));
}where the deduce_variant_t must account for all tuple-like types, including
arrays.
template <typename, typename>
struct deduce_variant;
template <typename T, size_t... Index>
struct deduce_variant<T, index_sequence<Index...>>
{
using type = std::variant<tuple_element_t<Index, T>...>;
};
template <typename T>
using deduce_variant_t
= typename deduce_variant<T, make_index_sequence<tuple_size_v<T>>>::type;Again, it would be a good idea to remove duplicated types.
Variadic visitation works in a similar way to std::visit.
It builds a look-up table to map the run-time index into a typed function
whose signature contains the compile-time index.
An exception is thrown if the run-time index is out-of-bounds.
template <typename Visitor,
typename... Args,
R = common_type_t<invoke_result_t<Visitor>, Args>...>>
constexpr R variadic_visit(Visitor&& v, size_t index, Args&&... args)
{
// Immediately invoked function object
return (index < sizeof...(Args))
? variadic_visit_table<R, index_sequence_for<Args...>, Visitor, Args...>{}
(forward<Visitor>(v), index, forward<Args>(args)...)
: throw out_of_range{"variadic_visit"};
}The return type R of variadic visitation is deduced by considering
the return types of all the potential visitor invocations.
The variadic_visit_table helper contains the look-up table and does
all the necessary type erasure.
The injected integer sequence is needed to initialize array.
template <typename, typename, typename, typename...>
struct variadic_visit_table;
template <typename R, size_t... Index, typename Visitor, typename... Args>
struct variadic_visit_table<R, index_sequence<Index...>, Visitor, Args...>
{
private:
template <size_t N>
static constexpr R indexed_invoke(Visitor&& v, Args&&... args)
{
// Invokes visitor with Nth element
return invoke_r<R>(forward<Visitor>(v),
pack_at(integral_constant<size_t, N>{},
forward<Args>(args)...));
}
// Array of function pointers
decltype(&indexed_invoke<0>) table[sizeof...(Index)];
public:
constexpr variadic_visit_table()
: table{ &indexed_invoke<Index>... } // Parameter pack expansion
{
}
// Preconditions:
// - index is within bounds.
constexpr R operator()(Visitor&& v, size_t index, Args&&... args)
{
return table[index](forward<Visitor>(v), forward<Args>(args)...);
}
};The pack_at function is similar to that defined earlier, but needs to
be back-ported from C++26 to earlier standards.
template <typename... Args>
auto pack_at(integral_constant<size_t, N> index, Args&&... args)
{
#if __cpp_pack_indexing >= 202311L
return args...[N];
#else
return get<N>(forward_as_tuple(forward<Args>(args)...));
#endif
}Caveat: The above has glossed over some technical detail, such as using decltype(auto)
rather than auto as the return type.