Beginner
Beginner

Reputation: 5477

Efficient and simple comparison operators for structs

The application I am working on currently has a large number structs which contain data which is input from various sources such as data bases and files. For example like this:

struct A 
{
    float val1;
    std::string val2;
    int val3;

    bool operator < (const A& other) const;
};

For processing, these structs are stored up in STL-containers, such as maps and therefore need a comparison operator. These are all the same and using simple boolean logic they can be written like this:

bool A:operator < (const A& o) const {
    return val1 < o.val1 || 
        (val1 == o.val1 && ( val2 < o.val2 || 
            (val2 == o.val2 && ( val3 < o.val3 ) ) );
}

This seems efficient, but has several drawbacks:

  1. These expressions get huge if the structs as a dozen or more members.
  2. It is cumbersome to write and maintain if members change.
  3. It needs to be done for every struct separately.

Is there a more maintainable way to compare structs like this?

Upvotes: 20

Views: 2309

Answers (2)

Richard Hodges
Richard Hodges

Reputation: 69942

Great answer by lubgr.

One further refinement I perform is the creation of a member function as_tuple on any object which is to be ordered by its members:

#include <string>
#include <tuple>
#include <iostream>

struct A 
{
    float val1;
    std::string val2;
    int val3;

    // provide easy conversion to tuple
    auto as_tuple() const
    {
        return std::tie(val1, val2, val3);
    }
};

Which often gives rise to thoughts of a general system of making objects and tuples interchangeable in terms of comparisons

template<class T> auto as_tuple(T&& l) -> decltype(l.as_tuple()) 
{
    return l.as_tuple();
}

template<class...Ts> 
auto as_tuple(std::tuple<Ts...> const& tup) 
-> decltype(auto)
{
    return tup;
}

template<class L, class R>
auto operator < (L const& l, R const& r)
-> decltype(as_tuple(l), void(), as_tuple(r), void(), bool())
{
    return as_tuple(l) < as_tuple(r);
}

Which allows such code as:

int main()
{
    auto a = A { 1.1, "foo", 0 };
    auto b = A { 1.1, "foo", 1 };

    auto test1 = a < b;
    std::cout << test1 << std::endl;

    auto test2 = a < std::make_tuple(1.1, "bar", 0);
    std::cout << test2 << std::endl;

    auto test3 = std::make_tuple(1.0, "bar", 0) < std::make_tuple(1.1, "bar", 0);
    std::cout << test3 << std::endl;

    auto test4 = a < std::make_tuple(2l, std::string("bar"), 0);
    std::cout << test4 << std::endl;

}

example: http://coliru.stacked-crooked.com/a/ead750f3f65e3ee9

Upvotes: 9

lubgr
lubgr

Reputation: 38325

You can use the builtin comparison that ships with <tuple> like this:

#include <tuple>

bool A::operator < (const A& rhs) const {
    return std::tie(val1, val2, val3) < std::tie(rhs.val1, rhs.val2, rhs.val3);
}

This doesn't scale when more and more data members are added to the struct, but this might also be a hint that you could create intermediate structs that implement operator < and hence play well with the above implementation of a top-level operator <.

Let me add three additional comments on operator <.

  1. Once you have operator <, clients will expect that all other comparison operators are provided, too. Before we have the three-way comparison in C++20, you can avoid unnecessary boilerplate code by e.g. using the Boost operator library:

    #include <boost/operators.hpp>
    
    struct A : private boost::totally_ordered<A> { /* ... */ };
    

    which generates all operators based on operator < and operator == for you.

  2. In your example, there is no need for the operator to be a member of A. You can make it a free function, which is preferable (see here for the rationale).

  3. If there is no intrinsic ordering related to A and you just need operator < to store instances as keys in a std::map, consider providing a named predicate.

Upvotes: 25

Related Questions