To preface the answer, it might be vastly more straightforward to use Boost.Hana. The prerequisite for Hana is that your comparison produces a compile-time answer. In your case, this would require a Hana tuple containing compile-time versions of these basic data types, similar to std::integral_constant
. If it's acceptable to have your tuples' values encoded entirely in their types, Hana makes this trivial.
I believe it would be possible to do this directly once you can use a tuple as a non-type template parameter in C++20. Until then, you can get pretty close (live example):
int main() {
constexpr std::tuple<long, int, float> t(2,1,3);
call_with_sorted_tuple(t, [](const auto& sorted) {
assert((sorted == std::tuple<int, long, float>(1,2,3)));
});
}
As far as I know, it is impossible to return the sorted tuple directly; the callback approach is required because it is instantiated with every possible tuple type and only the correct one is actually run. This means there is significant compile-time overhead to this approach. The compile times grow quickly with small tuple size increases.
Now, how does this actually work? Let's get the magic out of the way—converting a runtime integral value into a compile-time one. This can fit well into its own header, and is shamelessly stolen from P0376:
// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0376r0.html
#include <array>
#include <type_traits>
#include <utility>
// A function that invokes the provided function with
// a std::integral_constant of the specified value and offset.
template <class ReturnType, class T, T Value, T Offset, class Fun>
constexpr ReturnType invoke_with_constant_impl(Fun&& fun) {
return std::forward<Fun>(fun)(
std::integral_constant<T, Value + Offset>());
}
// Indexes into a constexpr table of function pointers
template <template <class...> class ReturnTypeDeducer,
class T, T Offset, class Fun, class I, I... Indices>
constexpr decltype(auto) invoke_with_constant(Fun&& fun, T index,
std::integer_sequence<I, Indices...>) {
// Each invocation may potentially have a different return type, so we
// need to use the ReturnTypeDeducer to figure out what we should
// actually return.
using return_type
= ReturnTypeDeducer<
decltype(std::declval<Fun>()(std::integral_constant<T, Indices + Offset>()))...>;
return std::array<return_type(*)(Fun&&), sizeof...(Indices)>{
{{invoke_with_constant_impl<return_type, T, Indices, Offset, Fun>}...}}
[index - Offset](std::forward<Fun>(fun));
}
template <class T, T BeginValue, T EndValue>
struct to_constant_in_range_impl {
// Instantiations of "type" are used as the Provider
// template argument of argument_provider.
template <class U>
struct type
{
template <template <class...> class ReturnTypeDeducer, class Fun, class Self>
static constexpr decltype(auto) provide(Fun&& fun, Self&& self) {
return invoke_with_constant<ReturnTypeDeducer, T, BeginValue>(
std::forward<Fun>(fun),
std::forward<Self>(self).value,
std::make_index_sequence<EndValue - BeginValue>());
}
U&& value;
};
};
Now one thing to note is that I use C++20's ability to give lambdas template parameters simply because compilers already support this and it makes turning index_sequence
s into parameter packs really easy. The long way to do this is available prior to C++20, but a bit of an eyesore on top of code that's already hard enough to get through.
The sort itself isn't too bad despite the tuple needing compile-time indices for std::get
(unless you reuse the above magic, but all I have to say to that is yikes). You can change the algorithm as needed. You can even use a regular std::vector
in C++20 and push indices onto the back. What I chose to do is generate a std::array
containing the sorted-order indices of the tuple:
// I had trouble with constexpr std::swap library support on compilers.
template<typename T>
constexpr void constexpr_swap(T& a, T& b) {
auto temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
template<std::size_t I>
using index_c = std::integral_constant<std::size_t, I>;
template<typename... Ts>
constexpr auto get_index_order(const std::tuple<Ts...> tup) {
return [&]<std::size_t... Is>(std::index_sequence<Is...> is) {
std::array<std::size_t, sizeof...(Is)> indices{Is...};
auto do_swap = [&]<std::size_t I, std::size_t J>(index_c<I>, index_c<J>) {
if (J <= I) return;
if (std::get<I>(tup) < std::get<J>(tup)) return;
constexpr_swap(indices[I], indices[J]);
};
auto swap_with_min = [&]<std::size_t I, std::size_t... Js>(index_c<I> i, std::index_sequence<Js...>) {
(do_swap(i, index_c<Js>{}), ...);
};
(swap_with_min(index_c<Is>{}, is), ...);
return indices;
}(std::index_sequence_for<Ts...>{});
}
The main idea here is obtaining a pack of indices from 0 to N-1 and then dealing with each individually. Rather than trying to generate a second pack from I+1 to N-1, I took the easy road and reused the 0 to N-1 pack I already had, ignoring all out-of-order combinations when swapping. The dance with index_c
is to avoid calling the lambdas via awkward lambda.template operator()<...>(...)
syntax.
Now we have the indices of the tuple in sorted order and magic to convert one index to one with its value encoded in the type. Rather than build the magic to handle multiple values, I took the probably-suboptimal approach to build on the support for one at a time by making a recursive function:
template<typename... Ts, typename F, std::size_t... Converted>
constexpr void convert_or_call(const std::tuple<Ts...> tup, F f, const std::array<std::size_t, sizeof...(Ts)>& index_order, std::index_sequence<Converted...>) {
using Range = typename to_constant_in_range_impl<std::size_t, 0, sizeof...(Ts)>::template type<const std::size_t&>;
if constexpr (sizeof...(Converted) == sizeof...(Ts)) {
f(std::tuple{std::get<Converted>(tup)...});
} else {
Range r{index_order[sizeof...(Converted)]};
r.template provide<std::void_t>([&]<std::size_t Next>(index_c<Next>) {
convert_or_call(tup, f, index_order, std::index_sequence<Converted..., Next>{});
}, r);
}
}
I would have made this a lambda to avoid repeating captures, but as its recursive, it needs a workaround to call itself in lambda form. I'd be happy to hear of a good, constexpr-compatible solution for lambdas in this case that takes into account the fact that the lambda's template arguments differ each call.
Anyway, this is the use of the magic. We want to call this a total of N times, where N is the tuple size. That's what the if constexpr
checks for, and finally delegates to the function passed from main
, easily building a new tuple from the compile-time index order sequence. To recurse, we add on this compile-time index to a list we build up.
Finally, since what should have been a lambda is its own function, the function called from main
is a simple wrapper that gets the index-order array and starts off the runtime-sequence-to-compile-time-sequence recursion with nothing converted to start:
template<typename... Ts, typename F>
constexpr void call_with_sorted_tuple(const std::tuple<Ts...>& tup, F f) {
auto index_order = get_index_order(tup);
convert_or_call(tup, f, index_order, std::index_sequence<>{});
}