First, you should seriously consider if you want keep you design (also in boost/operators.hpp) where a class inherits its operators from a base, or the design of std::rel_ops and Edwards's answer, where operators are defined as generic function templates. I think there are plus'es and minus'es, so there is no clear winner.
Assuming you want to keep your design, I find two issues:
It's better to design operators to match exactly rather than require conversions; otherwise, ambiguities and surprises will come sooner or later. One way to achieve this while being able to override the default behaviour is to separate interface from implementation.
This class makes things more convenient for the programmer, but officially accepts their laziness, at the cost of runtime performance. It is also possible to reduce code through implementation that is generic with respect to operators, at no performance loss.
Here is my suggestion, dealing with both issues, but staying as close as possible to your design:
template<typename T>
struct relational
{
// cast to base/derived class
static const relational&
base(T const& a) { return static_cast <const relational&>(a); }
const T& der() const { return static_cast<const T&>(*this); }
// interface: relational operators
friend bool operator< (T const &a, T const &b) { return base(a).compare(op::lt(), b); }
friend bool operator> (T const &a, T const &b) { return base(a).compare(op::gt(), b); }
friend bool operator==(T const &a, T const &b) { return base(a).compare(op::eq(), b); }
friend bool operator!=(T const &a, T const &b) { return base(a).compare(op::ne(), b); }
friend bool operator<=(T const &a, T const &b) { return base(a).compare(op::le(), b); }
friend bool operator>=(T const &a, T const &b) { return base(a).compare(op::ge(), b); }
// private dispatcher, to be called by relational operators
template<typename F>
bool compare(F f, T const &b) const { return der().comp(f, b); }
protected:
// protected implementation, possibly overriden, to be called by compare
bool comp(op::gt, T const &b) const { return b < der(); }
bool comp(op::eq, T const &b) const { return !(der() < b || der() > b); }
bool comp(op::ne, T const &b) const { return !(b == der()); }
bool comp(op::le, T const &b) const { return !(b < der()); }
bool comp(op::ge, T const &b) const { return !(der() < b); }
};
Here, operators are defined for T
as in your original design and not for the base class as in your eventual workaround. When a derived class needs to define anything else than operator<
, it overrides comp
rather than defining a relational operator directly. comp
is the implementation. operator@
is the interface.
Operators are represented by function objects like op::lt
, op::gt
etc. More on them below. Hence a derived class can override just a single overload of comp
, or all of them with a template.
base
and compare
are not very important; they are only needed to make accessible to the operators what is accessible to relational
.
Let's look at an example:
template<typename... T>
class direct : std::tuple<T...>, public relational<direct<T...>>
{
friend relational<direct>;
using U = std::tuple<T...>;
const U& tup() const { return static_cast<const U&>(*this); }
// override all functions
template<typename F>
bool comp(F f, const direct &b) const { return f(tup(), b.tup()); }
public:
using U::U;
};
direct
derives an std::tuple
both for data storage and for providing lexicographical comparisons. It defines all comparison operators by overriding all overloads of comp
with a single template.
A second example is like direct
but reverses the relational operators:
template<typename... T>
class reverse : std::tuple<T...>, public relational<reverse<T...>>
{
friend relational<reverse>;
using relational<reverse>::comp;
using U = std::tuple<T...>;
const U& tup() const { return static_cast<const U&>(*this); }
// override operator< only, rest is automatically generated
bool comp(op::lt, const reverse &b) const { return tup() > b.tup(); }
public:
using U::U;
};
It overrides only one overload of comp
, the rest are as defined in the base class relational
, but this comes at a runtime cost. I call it the lazy version. Why? Because with a little more typing, we could avoid the cost:
template<typename... T>
class reverse : std::tuple<T...>, relational<reverse<T...>>
{
friend relational<reverse>;
using U = std::tuple<T...>;
const U& tup() const { return static_cast<const U&>(*this); }
// override all functions (in fact, only == and !=)
template<typename F>
bool comp(F f, const reverse &b) const { return f(tup(), b.tup()); }
// override remaining functions, each separately
bool comp(op::lt, const reverse &b) const { return tup() > b.tup(); }
bool comp(op::gt, const reverse &b) const { return tup() < b.tup(); }
bool comp(op::le, const reverse &b) const { return tup() >= b.tup(); }
bool comp(op::ge, const reverse &b) const { return tup() <= b.tup(); }
public:
using U::U;
};
This overrides all overloads of comp
, manually. Code is longer, but there is no runtime cost. It's better to do this if the cost of comparison is low. We might prefer the lazy version if the cost of high (e.g. lexicographical comparison with more the 10 elements), so our "laziness" penalty is proportionally small. One exception is operator==
(also inherited by !=
), which doubles the lexicographical comparison cost, so you'd better manually override ==
as well (then !=
will be fine). The point is: think about the cost of being lazy.
Here is how we would use the above examples:
direct <int,int> d1{5,2}, d2{6,1}, d3{6,5}, d4{6,5};
reverse<int,int> r1{5,2}, r2{6,1}, r3{6,5}, r4{6,5};
std::cout << (d1 > d2) << " " << (d2 >= d3) << " " << (d3 >= d4) << std::endl; // 0 0 1
std::cout << (r1 > r2) << " " << (r2 >= r3) << " " << (r3 >= r4) << std::endl; // 1 1 1
See also live example.
Now, the function objects are like std::less
etc., but slightly different:
namespace op
{
struct lt
{
template<typename A, typename B>
auto operator()(A&& a, B&& b) const
-> decltype(std::forward<A>(a) < std::forward<B>(b))
{ return std::forward<A>(a) < std::forward<B>(b); }
};
// similarly for gt, eq, ne, le, ge
}
These are as generic as possible, with perfect forwarding and automatically deduced return type (in case someone defines anything other than bool
). So they can be used anywhere, not just for this solution. If you are feeling lazy, you could generate them with macros.
std::rel_ops
. These are often criticized as "greedy and unfriendly" but I think this is only because they have only one template parameter. With two different parameters forlhs
andrhs
, I don't think there would be any ambiguities. – iavr Apr 30 '14 at 22:36