3

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

an_drade
  • 664
  • 1
  • 5
  • 15
  • At the call point, you need to specify which instantiation of `lesser` to use, not just whack the name of the template in. For example, `sort(all.begin(), all.end(), lesser<0, unsigned*, unsigned*, unsigned*, unsigned>())`. You can simplify that with help of a `typedef` (or equivalent `using`). – Peter Sep 28 '22 at 03:40
  • Thanks @Peter, but this is not generic enough. Imagine I have different tuples types like this, with dozens of pointers ( I have one with 12 pointers!). So, writing some code that could make the compiler deduce the parameters automatically would be nice. – an_drade Sep 28 '22 at 03:46
  • It would be much easier to put `unsigned` (identifier) in the first element of the tuple. – 康桓瑋 Sep 28 '22 at 04:13
  • I think it doesn't matter since I need to dereference the other elements anyway. But, it will be more clear, sure. I have a tuple with few elements in the first version of this code, so I could use the standard operator< . But, things evolved in another path, so I have to use pointers. – an_drade Sep 28 '22 at 04:20
  • 2
    Pack your comparator into a function object with a templated `operator()`. – n. m. could be an AI Sep 28 '22 at 04:22
  • If it is only used for sorting, regular `operator<` would do the job (you compare also id for *"equivalent"* element). (for `std::map`, it would indeed be problematic as you would have the *"duplicated"* element in the map too). – Jarod42 Sep 28 '22 at 08:13

3 Answers3

1

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;
}
joergbrech
  • 2,056
  • 1
  • 5
  • 17
  • If you have C++20, this would be a nice use of the spaceship operator. The ordering of your `Ptr` type is defined by the ordering of the value type of the pointer. https://godbolt.org/z/9eenqsG7b – joergbrech Sep 28 '22 at 05:44
  • Thanks @joergbrech. I think the `struct Less` give us some flexibility but add another helper struct in the code. In some sense, it is similar to the "interface and implementation functions" idiom, commonly found when manipulating tuples. The pointer wrapper is indeed a cleaner version, mainly when are using complex objects. To use standard operators is often a better alternative than building your own. However, I wonder about the computational overhead it takes when using only simple fundamental types. – an_drade Sep 29 '22 at 17:56
1

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);
        }
        
    }
};
GAVD
  • 1,977
  • 3
  • 22
  • 40
  • `std::tuple_element_t` can replace your `type_tuple`. – Jarod42 Sep 28 '22 at 08:15
  • Thanks @GAVD. I think this solution is very good, assuming I can instantiate `TupleCompare` using different (and standard) comparison functors. – an_drade Sep 29 '22 at 18:06
1

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 becomes, as the other solutions, wrapping a function in a functor.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Thanks @Jarod42. I have seen similar constructs using c++20. The good thing is that it is a one-liner, although the code is denser. I'm still thinking if I want to go there. – an_drade Sep 29 '22 at 17:47