8
\$\begingroup\$

Idea: In C++, often a straightforward implementation of comparison operators is needed. In C++11, they can be conveniently implemented using std::tie. The following mixin class automatically enables comparison operators by deriving a class T from Comparable<T>.

Implementation: (please review this part, requires C++14)

// A mixin class that makes derived classes comparable
template<typename T>
struct Comparable {
    // nothing is needed here
};

// Default: Assume the client class offers a tie method
// Alternative: The client class can override this method
template<typename T>
auto tie(const Comparable<T>& a_object)
{
    return static_cast<const T&>(a_object).tie();
}

template<typename T>
bool operator==(const Comparable<T>& a_lhs, const Comparable<T>& a_rhs)
{
    return tie(a_lhs) == tie(a_rhs);
}

template<typename T>
bool operator!=(const Comparable<T>& a_lhs, const Comparable<T>& a_rhs)
{
    return tie(a_lhs) != tie(a_rhs);
}

template<typename T>
bool operator<(const Comparable<T>& a_lhs, const Comparable<T>& a_rhs)
{
    return tie(a_lhs) < tie(a_rhs);
}

template<typename T>
bool operator>(const Comparable<T>& a_lhs, const Comparable<T>& a_rhs)
{
    return tie(a_lhs) > tie(a_rhs);
}

template<typename T>
bool operator<=(const Comparable<T>& a_lhs, const Comparable<T>& a_rhs)
{
    return tie(a_lhs) <= tie(a_rhs);
}

template<typename T>
bool operator>=(const Comparable<T>& a_lhs, const Comparable<T>& a_rhs)
{
    return tie(a_lhs) >= tie(a_rhs);
}

Usage Example: Client code then could look like the following. The implementation of the tie function automatically defines a lexicographical ordering for all supported operations.

struct Client : public Comparable<Client>
{
    auto tie() const
    {
        return std::tie(m_a, m_b);
    }

    int m_a = 1;
    int m_b = 2;
};

Client a, b;
b.m_b = 5;

const bool isEqual = (a == b);
const bool isNotEqual = (a != b);
const bool isSmaller = (a < b);
const bool isGreater = (a > b);
const bool isSmallerOrEqual = (a <= b);
const bool isGreaterOrEqual = (a >= b);

Advantage: There is no need anymore to implement many comparison operators by hand for many classes individually. If there are many different classes requiring comparison operators with straightforward semantics this can save quite some code.

\$\endgroup\$
  • \$\begingroup\$ It's not really needed, but if you like very compact code you can reduce the 6 comparison methods to only 1 by using a simple macro over ==,<,>,>=,<=,!=. \$\endgroup\$ – Winther Dec 1 '15 at 20:53
2
\$\begingroup\$

ADL

This can be improved. First - you're requiring a tie() member function, it'd be better to make that more of a customization point. Second - you're putting all the operators in global scope when it'd be better to hide them. Both issues can be solved with some ADL.

The advantage of making an operator a non-member friend declared inline is that it can only be found when comparing precisely what you want. The downside to what you're doing right now is the following compiles:

struct B : Comparable<B> {
    ...
};

struct D {
    operator B() { ... }
};

D d1 = ...;
D d2 = ....;
if (d1 == d2) { ... }

D isn't comparable - but this code would still compile via the B conversion, which may be surprising!

We can instead write equality like so:

namespace cmp {
    template <class T>
    auto tie(const T& val) -> decltype(val.tie()) {
        return val.tie();
    }

    template <class T>
    struct Comparable {
        friend bool operator==(const T& lhs, const T& rhs) {
            return tie(lhs) == tie(rhs);
        }
    };
}

Note that the operator here takes Ts and not Comparable<T>s. We know what the derived type is, so we can just use it. The nonqualified call to tie lets us write something like:

namespace foo {
    struct B : Comparable<B> {
        int i;
    };

    int tie(const B& b) { return b.i; }
}

Customization is nice.

Split it up

EqualityComparable and LessThanComparable are different concepts. If you split up the responsibilities, it'll make your class more useful:

namespace cmp {
    template <class T>
    struct EqualityComparable {
        friend bool operator==(const T& lhs, const T& rhs) { return tie(lhs) == tie(rhs); }
        friend bool operator!=(const T& lhs, const T& rhs) { !(lhs == rhs); }
    };

    template <class T>
    struct LessThanComparable {
        friend bool operator<(const T& lhs, const T& rhs) { return tie(lhs) < tie(rhs); }
        friend bool operator<=(const T& lhs, const T& rhs) { return !(rhs < lhs); }
        // etc.
    };

    template <class T>
    struct TotallyOrdered : EqualityComparable<T>, LessThanComparable<T>
    { };
}

Cross-Compare

If two classes have comparable tie()s, why not support that too?

template <class T, class U>
struct EqualityComparable2
{
    friend bool operator==(const T& lhs, const U& rhs) { return tie(lhs) == tie(rhs); }
    friend bool operator==(const U& lhs, const T& rhs) { return rhs == lhs; }
    // same for !=
};

Go wild.

\$\endgroup\$
  • \$\begingroup\$ Is there a reason to replace std::tie by std::make_tuple? std::tie should avoid the copies, no? \$\endgroup\$ – SebastianK Dec 1 '15 at 23:08
  • \$\begingroup\$ @SebastianK For int, doesn't matter. \$\endgroup\$ – Barry Dec 1 '15 at 23:12
  • \$\begingroup\$ Thank for your great remarks. However, how is D prevented to be compared? I don't see this in your solution, comparing D's does compile. \$\endgroup\$ – SebastianK Dec 2 '15 at 12:44
  • \$\begingroup\$ @SebastianK Er, wrong counterexample. Fixed. \$\endgroup\$ – Barry Dec 2 '15 at 14:54
  • \$\begingroup\$ So the original example remains a problem? \$\endgroup\$ – SebastianK Dec 2 '15 at 20:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.