Reputation: 21514
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?
operator<
taking a parameter? (would be used as a <(ByTime) b
)?lowerThan
(comparing values) method and a earlierThan
(comparing time) method taking the right operand as parameter? But then, what would be the best practice to handle <
, <=
, >
, >=
, ==
, !=
, should I have one method for each comparator? Or may they take parameters (like bool isLower(bool strict, const A& right) const
, bool isGreater(bool strict, const A& right) const
, bool isEarlier(bool strict, const A& right) const
, bool isLater(bool strict, const A& right) const
...What would be the best practice?
Upvotes: 1
Views: 176
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
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
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
Reputation: 69882
IMHO the most versatile way is a 2-step process:
make ADL getters.
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