Home > Net >  Custom tuple comparator for std::sort()
Custom tuple comparator for std::sort()

Time:09-28

I have the following situation: I must pack several pointers and an identifier into a tuple like this:

typedef tuple<unsigned*, unsigned*, unsigned*, unsigned> tuple_with_pointers_t;

Here, I have three pointers and one id. In other situations, I may have more or fewer pointers, but the last will be the id. Note that I used unsigned* as an example only. It could be more complex objects.

Now, I want to compare the values of two such tuples. I.e., I need to dereference all tuple elements but the last. We can archive this using the following (in C 17):

template <size_t I = 0, typename T, typename... Ts>
constexpr bool lesser(std::tuple<T, Ts...> a, std::tuple<T, Ts...> b)
{
    if constexpr (I < sizeof...(Ts))
        return (*std::get<I>(a) < *std::get<I>(b)) ||
               ((*std::get<I>(a) == *std::get<I>(b)) && lesser<I   1>(a, b));
    else
        return std::get<I>(a) < std::get<I>(b);
}

Such construct works very fine when we compare two tuples directly. Now, I would like to use lesser() as a functor on the std::sort(). But, both g and clang complain that they cannot "couldn't infer template argument '_Compare'". In other words, we need to pass the correct template arguments to lesser.

I have tried some things here, but with no success: we have three template parameters, and I am not sure how I can use the _Elements from the tuple here. What will be the best strategy?

Here is some toy code:

#include <algorithm>
#include <iostream>
#include <tuple>
#include <vector>

using namespace std;

// My weird tuple with pointers and one unsigned index.
typedef tuple<unsigned*, unsigned*, unsigned*, unsigned> tuple_with_pointers_t;

// This works fine for two tuples directly. Note that we cannot dereference
// the last tuple element, so we compare it directly.
template <size_t I = 0, typename T, typename... Ts>
constexpr bool lesser(std::tuple<T, Ts...> a, std::tuple<T, Ts...> b)
{
    if constexpr (I < sizeof...(Ts))
        return (*std::get<I>(a) < *std::get<I>(b)) ||
               ((*std::get<I>(a) == *std::get<I>(b)) && lesser<I   1>(a, b));
    else
        return std::get<I>(a) < std::get<I>(b);
}

int main() {
    // Three sets of values.
    vector<unsigned> values1 {1, 2, 3};
    vector<unsigned> values2 {10, 20, 30};
    vector<unsigned> values3 {11, 22, 33};

    // Here, we pack it all together with the index.
    vector<tuple_with_pointers_t> all;

    for(unsigned i = 0; i < values1.size();   i)
        all.emplace_back(&values1[i], &values2[i], &values3[i], i);


    // So, it works if we want to compare two elements of our vector.
    cout << "\n- t0 < t1: " << std::boolalpha << lesser(all[0], all[1]);
    cout << "\n- t2 < t1: " << std::boolalpha << lesser(all[2], all[1]);


    // Now, I want to sort the tuples by their values. The compiler doesn't
    // like it: it cannot deduce the template parameters.
    sort(all.begin(), all.end(), lesser);

    return 0;
}

I appreciate any help, either using C 17 or C 20. But I'm looking for the most compact and elegant way to do it. It could be using a lambda function directly on the sort() call, too, if possible.

Thanks!

Update:

OK, I found a little hack that works:

sort(all.begin(), all.end(),
     [](const auto &a, const auto &b) {
        return lesser(a, b);
     }
);

Basically, we wrap it into a lambda, and therefore the compiler can deduce the types. But, can we do better?

Thanks

CodePudding user response:

As suggested in the comments, you can add your comparator into a function object and pass an instance of the object to sort:

#include <algorithm>
#include <iostream>
#include <tuple>
#include <vector>

using namespace std;

// My weird tuple with pointers and one unsigned index.
typedef tuple<unsigned*, unsigned*, unsigned*, unsigned> tuple_with_pointers_t;

namespace details {

template <size_t I = 0, typename T, typename... Ts>
constexpr bool lesser(std::tuple<T, Ts...> const& a, std::tuple<T, Ts...> const& b)
{
    if constexpr (I < sizeof...(Ts))
        return (*std::get<I>(a) < *std::get<I>(b)) ||
               ((*std::get<I>(a) == *std::get<I>(b)) && lesser<I   1>(a, b));
    else
        return std::get<I>(a) < std::get<I>(b);
}
}

struct Less
{
    template <typename... Ts>
    constexpr bool operator()(std::tuple<Ts...> const& a, std::tuple<Ts...> const& b)
    {
        return details::lesser<0, Ts...>(a, b);
    }
};

int main() {
    // Three sets of values.
    vector<unsigned> values1 {1, 2, 3};
    vector<unsigned> values2 {10, 20, 30};
    vector<unsigned> values3 {11, 22, 33};

    // Here, we pack it all together with the index.
    vector<tuple_with_pointers_t> all;

    for(unsigned i = 0; i < values1.size();   i)
        all.emplace_back(&values1[i], &values2[i], &values3[i], i);


    // So, it works if we want to compare two elements of our vector.
    cout << "\n- t0 < t1: " << std::boolalpha << Less()(all[0], all[1]);
    cout << "\n- t2 < t1: " << std::boolalpha << Less()(all[2], all[1]);


    // Now, I want to sort the tuples by their values. The compiler doesn't
    // like it: it cannot deduce the template parameters.
    sort(all.begin(), all.end(), Less());

    return 0;
}

As an alternative, you could wrap your unsigned* in a custom pointer type and provide a comparator for it. Then you can use the default comperator for tuples, which compares the elements lexicographically.

I personally would prefer this approach, because the code is much more readable. I don't know if this would break your existing code or would entail a huge refactor.

#include <algorithm>
#include <iostream>
#include <tuple>
#include <vector>

using namespace std;

class Ptr
{
public:
    Ptr(unsigned& v) : m_ptr(&v) {}
    unsigned operator*() const {
        return *m_ptr;
    }
private:
    unsigned* m_ptr;
};

bool operator<(Ptr const& l, Ptr const& r)
{
    return *l < *r;
}

// My weird tuple with pointers and one unsigned index.
typedef tuple<Ptr, Ptr, Ptr, unsigned> tuple_with_pointers_t;

int main() {
    // Three sets of values.
    vector<unsigned> values1 {1, 2, 3};
    vector<unsigned> values2 {10, 20, 30};
    vector<unsigned> values3 {11, 22, 33};

    // Here, we pack it all together with the index.
    vector<tuple_with_pointers_t> all;

    for(unsigned i = 0; i < values1.size();   i)
        all.emplace_back(values1[i], values2[i], values3[i], i);


    // So, it works if we want to compare two elements of our vector.
    cout << "\n- t0 < t1: " << std::boolalpha << (all[0] < all[1]);
    cout << "\n- t2 < t1: " << std::boolalpha << (all[2] < all[1]);

    sort(all.begin(), all.end());

    return 0;
}

CodePudding user response:

I think we can use this. Of course, I don't know your tuple can be more complex.

template<typename T, size_t I = 0>
using type_tuple = typename std::tuple_element<I,T>::type;

template<size_t I = 0, template<typename> class F = std::less_equal>
struct TupleCompare
{
    template<typename T>
    bool operator()(T const &t1, T const &t2){
        using _type = typename std::conditional<std::is_pointer<type_tuple<T>>::value, 
            typename std::remove_pointer<type_tuple<T,I>>::type, type_tuple<T>>::type;

        if constexpr (I == std::tuple_size_v<T> - 1) {            
            return F<_type>()(std::get<I>(t1), std::get<I>(t2));
        } else {            
            return F<_type>()(*std::get<I>(t1), *std::get<I>(t2)) && TupleCompare<I 1, F>()(t1, t2);
        }
        
    }
};

CodePudding user response:

By doing a non-"recursive" function, you might do a "one-liner":

sort(all.begin(), all.end(),
     []<typename T>(const T& lhs, const T& rhs) {
         return [&]<std::size_t... Is>(std::index_sequence<Is...>){
             return std::tie(std::get<Is>(lhs)...)
                  < std::tie(std::get<Is>(rhs)...);
         }(std::make_index_sequence<std::tuple_size_v<T> - 1>{});
     });

template lambda are C 20.
Without that, at least an helper function is required, so it become, as the other solutions, wrapping a function in a functor.

  • Related