jpo38
jpo38

Reputation: 21514

What's the best strategy to provide different comparison operators for the same class?

Consider this simple class storing a value and a time.

class A
{
public:
    boost::posix_time::ptime when;
    double value;
};

Depending on the context, I need to compare two instances of A by value or by time (and/or store them in set/map, sometimes sorted by value, sometimes by time).

Providing operator< will be confusing, because you can't tell if it will compare by value or by time.

Now, what's the best strategy?

What would be the best practice?

Upvotes: 1

Views: 176

Answers (4)

jpo38
jpo38

Reputation: 21514

Another approach, very simple: add template comparator functions to the A class makes it easy to do a comparison in the end and is really error prone:

#include <iostream>
#include <set>

using namespace std;

class A
{
public:
    int when;
    double value;

    int getTime() const { return when; }
    double getValue() const { return value; }

    template<typename T>
    bool isLower( T (A::*getter)() const,
                  bool strict,
                  const A& right ) const
    {
        if ( strict )
            return ((*this).*getter)() < (right.*getter)();
        else
            return ((*this).*getter)() <= (right.*getter)();
    }

    template<typename T>
    bool isGreater( T (A::*getter)() const,
                    bool strict,
                    const A& right ) const
    {
        if ( strict )
            return ((*this).*getter)() > (right.*getter)();
        else
            return ((*this).*getter)() >= (right.*getter)();
    }

    template<typename T>
    bool isEqual( T (A::*getter)() const,
                  const A& right ) const
    {
        return ((*this).*getter)() == (right.*getter)();                  
    }
};

struct byTime_compare {
    bool operator() (const A& lhs, const A& rhs) const {
        return lhs.isLower( &A::getTime, true, rhs );
    }
};

int main()
{
    A a, b;

    if ( a.isLower( &A::getValue, true, b ) ) // means a < b by value
    {
        // ...
    }

    std::set<A, byTime_compare> mySet;
}

Upvotes: 0

Nyashes
Nyashes

Reputation: 652

short answer: don't I explained why in a comment, the main reason is, it introduces ambiguity in your code and reduces readability which is the opposite of what operators are meant to do. Just use different methods and provide ways to pick which one to use for this sort (like comparers). While I was typing this, people posted good examples of that, even some using a bit of metaprogramming.

however, for science, you kinda can. While you can't add a parameter to an operator (a binary operator is a binary operator, and there doesn't seem to be a syntax to add this third argument somewhere) you can make your operator mean different things in different contexts (c++ context, for a line of code or for a block delimited by '{}')

here done very quickly using construction/destruction order (similar implementation to a trivial lock with no consideration for thread safety):

the comparison looks like:

Thing::thingSortingMode(Thing::thingSortingMode::alternateMode), Thing{1, 2} < Thing{3, 4};

run this example online: http://cpp.sh/3ggrq

#include <iostream>

struct Thing {
    struct thingSortingMode {
        enum mode {
            defaultMode,
            alternateMode
        };

        mode myLastMode;

        thingSortingMode(mode aMode) { myLastMode = Thing::ourSortingMode; Thing::ourSortingMode = aMode; std::cout << "\nmode: " << aMode << "\n"; }
        ~thingSortingMode() { Thing::ourSortingMode = myLastMode; std::cout << "\nmode: " << myLastMode << "\n";}
    };

    bool operator < (Thing another) {
        switch (ourSortingMode) //I use an enum, to make the example more accessible, you can use a functor instead if you want
        {
            case thingSortingMode::alternateMode:
                return myValueB < another.myValueB;
                break;

            default:
                return myValueA < another.myValueA;
                break;
        }
    }

    static thingSortingMode::mode ourSortingMode;

    int myValueA;
    int myValueB;
};

Thing::thingSortingMode::mode Thing::ourSortingMode = Thing::thingSortingMode::defaultMode;

int main()
{
  Thing a{1, 1}, b{0, 2}; // b < a in default mode, a < b in alternate mode

  std::cout << (a < b); //false

  {
    Thing::thingSortingMode ctx(Thing::thingSortingMode::alternateMode);


    std::cout << (a < b); //true
    Thing::thingSortingMode(Thing::thingSortingMode::defaultMode), std::cout << (a < b), //false
        Thing::thingSortingMode(Thing::thingSortingMode::alternateMode), std::cout << (a < b); //true

    std::cout << (a < b); //true
  }

  std::cout << (a < b); //false
}

Note that this construction/destruction trick can manage any kind of contextual state, here is a richer example with 4 states and more nested contexts

run this example online: http://cpp.sh/2x5rj

#include <iostream>

struct Thing {
    struct thingSortingMode {
        enum mode {
            defaultMode = 1,
            alternateMode,
            mode3,
            mode4,
        };

        mode myLastMode;

        thingSortingMode(mode aMode) { myLastMode = Thing::ourSortingMode; Thing::ourSortingMode = aMode; std::cout << "\nmode: " << myLastMode << " -> " << aMode << "\n"; }
        ~thingSortingMode() { std::cout << "\nmode: " << Thing::ourSortingMode << " -> " << myLastMode << "\n"; Thing::ourSortingMode = myLastMode; }
    };

    static thingSortingMode::mode ourSortingMode;
};

Thing::thingSortingMode::mode Thing::ourSortingMode = Thing::thingSortingMode::defaultMode;

int main()
{
    Thing::thingSortingMode ctx(Thing::thingSortingMode::mode3);
    {
        Thing::thingSortingMode ctx(Thing::thingSortingMode::alternateMode);
        {
            Thing::thingSortingMode ctx(Thing::thingSortingMode::mode4);
            {
                Thing::thingSortingMode ctx(Thing::thingSortingMode::defaultMode);
                std::cout << "end sub 3 (mode 1)\n";
            }

            std::cout << 
                (Thing::thingSortingMode(Thing::thingSortingMode::alternateMode), "this is the kind of things that might behave strangely\n") <<
                (Thing::thingSortingMode(Thing::thingSortingMode::defaultMode), "here both are printed in mode 2, but it's a direct consequence of the order in which this expression is evaluated\n"); //note though that arguments are still constructed in the right state

            std::cout << "end sub 2 (mode 4). Not that we still pop our states in the right order, even if we screwed up the previous line\n";
        }

        std::cout << 
                (Thing::thingSortingMode(Thing::thingSortingMode::alternateMode), "this on the other hand (mode 2)\n"),
        std::cout << 
                (Thing::thingSortingMode(Thing::thingSortingMode::defaultMode), "works (mode 1)\n"); //but pay attention to the comma and in which order things are deleted

        std::cout << "end sub 1 (mode 2)\n";
    }
    std::cout << "end main (mode 3)\n";
}

output:

mode: 1 -> 3

mode: 3 -> 2

mode: 2 -> 4

mode: 4 -> 1
end sub 3 (mode 1)

mode: 1 -> 4

mode: 4 -> 1

mode: 1 -> 2
this is the kind of things that might behave strangely
here both are printed in mode 2, but it's a direct consequence of the order in which this expression is evaluated

mode: 2 -> 1

mode: 1 -> 4
end sub 2 (mode 4). Not that we still pop our states in the right order, even if we screwed up the previous line

mode: 4 -> 2

mode: 2 -> 2
this on the other hand (mode 2)

mode: 2 -> 1
works (mode 1)

mode: 1 -> 2

mode: 2 -> 2
end sub 1 (mode 2)

mode: 2 -> 3
end main (mode 3)

mode: 3 -> 1

Upvotes: 0

jpo38
jpo38

Reputation: 21514

Reacting to UKMonkey comment, would defining what I understand could be named "comparator classes" be a good approach/practice?

class A
{
public:
    boost::posix_time::ptime when;
    double value;

    const boost::posix_time::ptime& getTime() const { return when; }
    double getValue() const { return value; }
};

template <typename T>
class CompareBy
{
public:
    CompareBy( const A& a, T (A::*getter)() const ) : a(a), getter(getter)
    {}

    bool operator<( const CompareBy& right ) const
    {
        return (a.*getter)() < (right.a.*getter)();
    }

    // you may also declare >, <=, >=, ==, != operators here

private:
    const A& a;
    T (A::*getter)() const;
};

class CompareByTime : public CompareBy<const boost::posix_time::ptime&>
{
public:
    CompareByTime(const A& a) : CompareBy(a, &A::getTime)
    {
    }
};

class CompareByValue : public CompareBy<double>
{
public:
    CompareByValue( const A& a ) : CompareBy(a, &A::getValue)
    {
    }
};

struct byTime_compare {
    bool operator() (const A& lhs, const A& rhs) const {
        return CompareByTime(lhs) < CompareByTime(rhs);
    }
};

int main()
{
    A a, b;

    ...

    if (CompareByValue(a) < CompareByValue(b))
    {
        ...
    }

    std::set<A, byTime_compare> mySet;
}

Upvotes: 1

Richard Hodges
Richard Hodges

Reputation: 69882

IMHO the most versatile way is a 2-step process:

  1. make ADL getters.

  2. write comparison concepts in terms of those getters.

example:

#include <boost/date_time.hpp>
#include <set>
#include <vector>
#include <algorithm>

class A
{
public:
    boost::posix_time::ptime when;
    double value;
};

// get the 'when' from an A
auto get_when(A const& a) -> boost::posix_time::ptime 
{ 
    return a.when; 
}

// get the 'when' from a ptime (you could put this in the boost::posix_time namespace for easy ADL    
auto get_when(boost::posix_time::ptime t) -> boost::posix_time::ptime 
{ 
    return t; 
}

// same for the concept of a 'value'
auto get_value(A const& a) -> double 
{ 
    return a.value; 
}

auto get_value(double t) -> double 
{ 
    return t; 
}

// compare any two objects by calling get_when() on them    
struct increasing_when
{
    template<class L, class R>
    bool operator()(L&& l, R&& r) const
    {
        return get_when(l) < get_when(r);
    }
};

// compare any two objects by calling get_value() on them    
struct increasing_value
{
    template<class L, class R>
    bool operator()(L&& l, R&& r) const
    {
        return get_value(l) < get_value(r);
    }
};


void example1(std::vector<A>& as)
{
    // sort by increasing when
    std::sort(begin(as), end(as), increasing_when());

    // sort by increasing value
    std::sort(begin(as), end(as), increasing_value());
}

int main()
{
    // same for associative collections
    std::set<A, increasing_when> a1;
    std::set<A, increasing_value> a2;
}

update:

If you want, you can templatise the comparison:

template<class Comp>
struct compare_when
{
    template<class L, class R>
    bool operator()(L&& l, R&& r) const
    {

        return comp(get_when(l), get_when(r));
    }

    Comp comp;
};    

using increasing_when = compare_when<std::less<>>;
using decreasing_when = compare_when<std::greater<>>;

to use the comparison directly in code:

auto comp = compare_when<std::greater<>>();
if (comp(x,y)) { ... }

Upvotes: 1

Related Questions