4

I am wondering if a constexpr std::tuple can be sorted at compile-time:

template<typename T>
struct A{ T val; }; // a constexpr-enabled class

constexpr auto t = std::make_tuple( A<int>{3}, A<float>{1.f}, A<double>{2.0});
constexpr auto s = sort(t, [](auto&& v){return v.val; });

static_assert(std::is_same_v<std::tuple<A<float>,
                                        A<double>, 
                                        A<int>>,decltype(s)>, "Wups");

Is that possible, which building blocks are needed here (std::sort is constexpr) ?

It is (as far as I know) impossible to do that at run-time, since one cannot determine the type of the sorted tuple at runtime. I cannot see a way also if one would build a type-map of all permutations, the actual permutation is still only known at runtime.

Answer https://stackoverflow.com/a/46006026/293195 gives some hints and it should work in compile-time, but how?

Gabriel
  • 8,990
  • 6
  • 57
  • 101

3 Answers3

5

Here's an ok solution using Boost.Mp11:

template <auto F>
struct quote {
    template <typename... T>
    using fn = decltype(F(T{}...));
};

constexpr auto sort_t() {
    // predicate used for sorting
    using pred = quote<[](auto I, auto J){
        return mp_bool<(std::get<I>(t).val < std::get<J>(t).val)>{};
    }>;

    // this gives us an mp_list of the indices into 't', sorted
    using indices = mp_sort<
        mp_iota<mp_int<std::tuple_size_v<decltype(t)>>>,
        pred::fn>;

    // turn those indices into a new tuple
    return []<typename... Ts>(mp_list<Ts...>){
        return std::tuple(std::get<Ts::value>(t)...);
    }(indices{});
}

constexpr auto s = sort_t();

The last step might be cleaned up a bit.


Note that ideally this takes t as a non-type template parameter, but we won't be able to do that until probably C++23:

template <auto Tuple>
constexpr auto sort_tuple() { /* ... */ }

constexpr auto s = sort_tuple<t>();

Although actually it can take an auto const& non-type template parameter, which is good enough here:

template <auto const& in>
constexpr auto sort_tuple() {
    using pred = quote<[](auto I, auto J){
        return mp_bool<(std::get<I>(in).val < std::get<J>(in).val)>{};
    }>;
    using indices = mp_sort_q<
        mp_iota_c<std::tuple_size_v<std::decay_t<decltype(in)>>>,
        pred>;

    return []<typename... Ts>(mp_list<Ts...>){
        return std::tuple(std::get<Ts::value>(in)...);
    }(indices{});
}
constexpr auto s = sort_tuple<t>();
Barry
  • 286,269
  • 29
  • 621
  • 977
  • Your link is broken. Do you know whether `mp_sort` will actually be efficient in the sense that it will instantiate `O(N ln N)` templates on average? – walnut Jan 02 '20 at 19:00
  • ... where `N` is the number of distinct types in the tuple. – walnut Jan 02 '20 at 19:06
  • @walnut Weird, dunno how the `l` got chopped off. Fixed the link. And yeah, I assume it's going to be efficient, everything else in the library is. – Barry Jan 02 '20 at 20:12
  • wow that is pretty neat :)! what i dont understand is: why pass by non-type template parameter. why is a reference to the tuple not working? because its then not a const. expr.? – Gabriel Jan 03 '20 at 00:43
  • @Gabriel Yes, the whole tuple has to be a constant expression. – Barry Jan 03 '20 at 01:26
  • Oh god, the lambda-quoting `quote<..>` is pretty neat :-). Basically changing from regular (constexpr) runtime world into the template world. You first need to wrap the head around this one :-). Cool. – Gabriel Jan 03 '20 at 10:40
3

You can make use of the standard library's std::sort algorithm (if it is constexpr as it should be in C++20), even if it expects homogeneous types by by wrapping only the indices of the tuple as distinct types in an array of std::variants and sorting on those.

You can then provide any predicate that you want, as long as comparing each pair of types in the tuple is possible with the predicate and it satisfies the usual requirements for comparators to std::sort.

In particular there is a std::less<void> specialization that accepts two arbitrary types for comparison with <, so I use that as default, similar to what std::sort does.

As mentioned by @Barry, you will however need to take the tuple as non-type template argument to access its values inside the function. If you want to access state of the predicate as well inside the sort algorithm, then you will need to pass it as reference non-type template argument as well.

Also note that this will probably take quadratic compilation time in the size of the tuple, because of the number of instantiations and visits for the comparator that will be required. For large tuples you can probably implement your own algorithm that doesn't need quadratically many instantiations using template metaprogramming that will only instantiate the required comparisons, instead.

Alternatively, the number of required instantiations can be reduced by copying the values (or std::refs to the values) contained in the tuple into the std::variants instead, after removing duplicate types, reducing the number of required instantiations and visits to quadratically many in the number of distinct types. See history of this answer for an incomplete implementation that did not actually deduplicate types and was therefore probably still quadratic in size of the tuple.

namespace detail {
    template<auto& t, typename Pred, auto... Is>
    constexpr auto sort(Pred&& pred, std::index_sequence<Is...>) {
        constexpr auto sorted_indices = [&]{
            using var_t = std::variant<std::integral_constant<std::size_t, Is>...>;

            std::array indices{var_t{std::in_place_index<Is>}...};

            std::sort(indices.begin(), indices.end(), [&](auto x, auto y){
                return std::visit([&](auto a, auto b) {
                    return pred(std::get<decltype(a)::value>(t), std::get<decltype(b)::value>(t));
                }, x, y);
            });

            return indices;
        }();

        using old_tuple_t = std::remove_cvref_t<decltype(t)>;
        using new_tuple_t = std::tuple<std::tuple_element_t<sorted_indices[Is].index(), old_tuple_t>...>;
        return new_tuple_t{std::get<sorted_indices[Is].index()>(t)...};
    }
}

template<auto& t, typename Pred = std::less<void>>
constexpr auto sort(Pred&& pred = {}) {
    using size = std::tuple_size<std::remove_cvref_t<decltype(t)>>;
    return detail::sort<t>(std::forward<Pred>(pred), std::make_index_sequence<size::value>{});
}

Tests (slightly corrected from the question and including some duplicate types):

template<typename T>
struct A{ T val; }; // a constexpr-enabled class

// Test case 1 comparing A's val members as predicate
constexpr auto t = std::make_tuple( A<int>{10}, A<float>{1.f}, A<double>{5.0}, A<int>{3});
constexpr auto s = sort<t>([](auto&& v, auto&& w){ return v.val < w.val; });
static_assert(std::is_same_v<const std::tuple<A<float>, A<int>, A<double>, A<int>>, decltype(s)>);

// Test case 2 using default `std::less<void>` for comparison
constexpr auto t2 = std::make_tuple(10, 1.f, 5.0, 3);
constexpr auto s2 = sort<t2>();
static_assert(std::is_same_v<const std::tuple<float, int, double, int>, decltype(s2)>);

Tested on GCC trunk in this godbolt. It doesn't work on Clang trunk with libstdc++ or libc++. From the error message produced, I assume it is because it doesn't implement constexpr std::sort yet with either.

walnut
  • 21,629
  • 4
  • 23
  • 59
0

Here (for the sake of completness) a cleaned up solution based on a mix of the excellent answers of @walnut and @Barry

Live on Wandbox

#include <type_traits>
#include <tuple>
#include "meta.hpp"

namespace details
{
    template<auto F>
    struct lambdaQuote
    {
        template<typename... T>
        using invoke = decltype(F(T{}...));
    };
}

template<auto& tuple, auto pred, bool doForward = false>
constexpr auto sort()
{
    using namespace meta;
    using namespace details;
    using Tuple = std::remove_cvref_t<decltype(tuple)>;

    using Pred = lambdaQuote<
        [](auto I, auto J) {
        return bool_<pred(std::get<I>(tuple), std::get<J>(tuple))>{};
    }>;

    using Indices       = as_list<make_index_sequence<std::tuple_size_v<Tuple>>>;
    using SortedIndices = meta::sort<Indices, Pred>;
    auto createTuple    = []<typename... Ts>(list<Ts...>)
    {
        if constexpr(doForward)
        {
            return std::forward_as_tuple(std::get<Ts::value>(tuple)...);
        }
        else
        {
            return std::make_tuple(std::get<Ts::value>(tuple)...);
        }
    };

    return createTuple(SortedIndices{});
}

template<typename T>
struct A
{
    constexpr A(T t) : val(t){};
    T val;
private:
    constexpr A() = default;
};


int main()
{
    static constexpr auto t = std::make_tuple(A{10.0},A{9},A{8.0f},A{7.0},A{6.0},A{5});
    constexpr auto p        = [](auto&& a, auto&& b) { return a.val < b.val; };
    constexpr auto s        = sort<t, p>();

    constexpr auto tRes = std::make_tuple(A{5},A{6.0},A{7.0},A{8.0f},A{9},A{10.0});
    static_assert(std::is_same_v<decltype(s), decltype(tRes)>, "not correct");
    static_assert(std::get<0>(tRes).val == std::get<0>(s).val, "not correct");
}
Gabriel
  • 8,990
  • 6
  • 57
  • 101