2

I know it's possible to use the C++ type system to generate a sorted type list from an existing tuple type.

Examples of doing this can be found at:

https://codereview.stackexchange.com/questions/131194/selection-sorting-a-type-list-compile-time

How to order types at compile-time?

However, is it possible to do a compile-time sort of a heterogeneous tuple by value? For example:

constexpr std::tuple<long, int, float> t(2,1,3);
constexpr std::tuple<int, long, float> t2 = tuple_sort(t);
assert(t2 == std::tuple<int, long, float>(1,2,3));

My assumption is this is not possible, since you'd have to conditionally generate new tuple types based on the result of comparing values. Even if the comparison function uses constexpr, it would seem that this can't work.

However, an offhand comment from this answer indicates it somehow is possible to do this, just very difficult:

I lied. You can do it if the values and the compare function are constexpr, but the code to pull it off will be huge and not worth the time to write.

So is this comment correct? How could this even be conceptually possible, given the way the C++ type system works.

R.J. Dunnill
  • 2,049
  • 3
  • 10
  • 21
Siler
  • 8,976
  • 11
  • 64
  • 124
  • If C++ templates are Turing complete then the answer is probably yes. But it might not be pretty. – wally Jul 24 '19 at 19:19
  • I would imagine you could accomplish this using an index sort. If you can get an `integer_sequence` of the final indexes then you can build a tuple of the sorted types using the `integer_sequence` and what type you would `get` from the index in the sequence. – NathanOliver Jul 24 '19 at 19:26
  • But creating an index_sequence has the same problem as creating a tuple... you need to conditionally generate a new type based on a value compare. Each index_sequence with different indices is an entirely different type – Siler Jul 24 '19 at 20:55

3 Answers3

3

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_sequences 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<>{});
}
chris
  • 60,560
  • 13
  • 143
  • 205
1

I believe, it can not be done.

The essential part of any sorting would be to use tuple value in if constexpr context, but since function arguments are not constexpr, they can not appear in if constexpr.

And since tuples can't be non-type template arguments, template-based solution can't be implemented either. Unless we make tuple of type-encoded values (like std::integral_constant) I believe the solution is not available.

SergeyA
  • 61,605
  • 5
  • 78
  • 137
0

Return type cannot depend of value of parameters (even more as parameter cannot be constexpr) of a function, so

constexpr std::tuple<long, int, float> t1(2, 1, 3);
constexpr std::tuple<long, int, float> t2(3, 2, 1);
static_assert(std::is_same<decltype(tuple_sort(t1), decltype(tuple_sort(t2)>::value, "!");
Jarod42
  • 203,559
  • 14
  • 181
  • 302