Shortcut for creating a comparator based on a member field or function

  • A+
Category:Languages

I often find myself wanting to create a comparator objects for a struct or class which simply extracts one member of the class and does the usual < comparison on that.

For example:

struct student {    int id;    std::string name; };  // sort by ID std::sort(students.begin(), students.end(), [](const student& l, const student& r){ return l.id < r.id; }); 

There's a lot of boilerplate there, in particular because we have to repeat the declaration for l and r. Is there a way in the standard library to create a comparator based on an "extractor" function which returns an object to compare on?

Something like:

std::sort(students.begin(), students.end(), compare_on([](const student& s){ return s.id; }); 

I'm using C++11, but also interested if there are solutions in later standards that don't apply in C++11 (so I can add something to my "reasons to upgrade" list).

I'm asking here about using a single member as the comprand, and also the default comparison "less than", but bonus points for techniques that compose easily, e.g. that allow you use two fields in lexicographic order, or to change the comparison operator.

 


You can define a utility class

template <class Fct> class compare_on {   public:     compare_on(Fct&& get) : get(std::forward<Fct>(get)) {}      template <class T> bool operator()(const T& lhs, const T& rhs)     {        return get(lhs) < get(rhs);     }    private:     Fct get; }; 

and then pass it to std::sort just like you depicted it (using C++17 class template argument deduction)

std::sort(students.begin(), students.end(),     compare_on([](const student& s){ return s.id; })); 

As of C++17, @Justin pointed out int the comments that the actual comparison can be improved (with #include <functional>) such that

return std::invoke(get, lhs) < std::invoke(get, rhs); 

which allows for an instantiation with data member references:

std::sort(students.begin(), students.end(), compare_on(&student::id)); 

When bound to C++11, forget about std::invoke and go with an explicit instantiation of compare_on. The latter isn't suitable for lambdas, hence the usual argument-deduction make_* helper:

template <class Fct> auto make_compare_on(Fct&& get)     -> decltype(compare_on<Fct>(std::forward<Fct>(get))) {     return compare_on<Fct>(std::forward<Fct>(get)); } 

Note that you can remove the trailing return type in C++14.

As a final note, the naming here should be improved: compare_on is misleading, as it hides what the function object really does - comparing by operator <. Maybe compare_less_then or something similar would be better, or adding another template parameter that can be specified as one of the standard predicates (std::less etc.).

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: