Template class implementing comparison operators - c++

It is a frequent task of mine to write all the overloaded comparison operators to a class, so I've written a template class which implements <,<=,>=,!= if the derived class implements == and <. It is working but features a lot of cast and the not that obvious "Curiously recurring template pattern", so I wonder if are there simpler solutions?
template <class Derived>
class Comparable
{
public:
bool operator!=(const Comparable<Derived>& other) {
return !(static_cast<Derived*>(this)->operator==
(*static_cast<const Derived*>(&other)));
}
bool operator<=(const Comparable<Derived>& other) {
return (static_cast<Derived*>(this)->operator==
(*static_cast<const Derived*>(&other)))
|| (static_cast<Derived*>(this)->operator<
(*static_cast<const Derived*>(&other)));
}
bool operator>(const Comparable<Derived>& other) {
return !(static_cast<Derived*>(this)->operator==
(*static_cast<const Derived*>(&other)))
&& !(static_cast<Derived*>(this)->operator<
(*static_cast<const Derived*>(&other)));
}
bool operator>=(const Comparable<Derived>& other) {
return !(static_cast<Derived*>(this)->operator<
(*static_cast<const Derived*>(&other)));
}
};

In case it is not obvious from the description in the comment:
template <typename T>
struct Comparable {
friend bool operator!=(T const & lhs, T const & rhs) { return !(lhs == rhs); }
friend bool operator> (T const & lhs, T const & rhs) { return rhs < lhs; }
// ...
};
class MyType : Comparable<MyType> {
int data;
friend bool operator==(MyType const & lhs, MyType const & rhs) {
return lhs.data == rhs.data;
}
friend bool operator< (MyType const & lhs, MyType const & rhs) {
return lhs.data < rhs.data;
}
public:
// ...
};
When the compiler encounters MyType a, b; a > b; lookup for the operator will end up doing ADL which will look inside MyType and Comparable<MyType> (as this is a base), where it will find the implementation you need: bool operator>(MyType const&, MyType const&).
The operators being free functions allows for a definition that is outside of the type that is being compared (in this case the base), while making those operators only available through ADL (one of the two arguments must be Comparable<MyType>). The use of a free function also provides type-symmetry, the compiler will allow implicit conversions on both sides, where in the case of a member function it would only allow conversions on the right hand side of the operator.
For completeness, a different trick that can be done is to provide the operators as templates in a namespace together with a tag that can be used to bring that namespace in for ADL purposes:
namespace operators {
template <typename T>
bool operator>(T const & lhs, T const & rhs) {
return rhs < lhs;
}
// rest of the operators come here
struct tag {};
}
class MyType : operators::tag {
int data;
friend bool operator<(T const & lhs, T const & rhs) {
return lhs.data < rhs.data;
}
//...
};
The trick is basically the same, except that in this case the operators are not found inside the base, but in a namespace that is associated with it. This solution is a bit less nice than the previous one, as it is open to different forms of misuse, including using namespace operators; that would make the templated operators available for all types.

Related

Generating comparison operators without CRTP

Let's consider class like that with less than operator:
struct Test
{
int value;
constexpr bool operator<(const Test& p_rhs) const
{
return value < p_rhs.value;
}
};
I would like to generate bool operator>(const Test&, const Test&),bool operator<=(const Test&, const Test&) and bool operator>=(const Test&, const Test&) without using CRTP(or any other inheritance) on Test class on generic way, that could be also used on other classes that have bool operator<(const T&,const T&). I tried sth like that:
template<typename Lhs, typename Rhs = Lhs>
struct GenerateOtherComparisonsFromLessThan
{
constexpr friend bool operator>(const Rhs& p_rhs, const Lhs& p_lhs)
{
return p_lhs < p_rhs;
}
//and rest of other comparison operators
};
struct InstantiateTestComparisons : GenerateOtherComparisonsFromLessThan<Test>
{};
But it looks like these overloads are not taken(at least on gcc 8.3 on ideone).

C++ relational operators generator

Once you define the < operator, you can have an estimation of how the rest of relational operators behave. I'm trying to implement a way to do that for my classes.
What I want is to define only the < and the rest of the operators to be defaulted implicitly. What I've got so far is this design, which I'll elaborate on further down:
template<typename T>
struct relational
{
friend bool operator> (T const &lhs, T const &rhs) { return rhs < lhs; }
friend bool operator==(T const &lhs, T const &rhs) { return !(lhs < rhs || lhs > rhs); }
friend bool operator!=(T const &lhs, T const &rhs) { return !(rhs == lhs); }
friend bool operator<=(T const &lhs, T const &rhs) { return !(rhs < lhs); }
friend bool operator>=(T const &lhs, T const &rhs) { return !(lhs < rhs); }
};
So for a class that implements the < operator it would just take inheriting from relational to have the rest of the operators defaulted.
struct foo : relational<foo>
{
// implement < operator here
};
Are there any alternatives, better designs ?
Is there a time bomb in this code? I'm assuming that if a user wants to define a custom implementation for one of the operators, the overload resolution would kick and select the non template (user defined) implementation. If that's not the case (or I would have problem with class templates inheriting from relational) should I implement the operators in relational like this ?
// inside the relational struct
friend bool operator>(relational const &lhs, relational const &rhs)
{ // functions that involve implicit conversion are less favourable in overload resolution
return (T const&)rhs < (T const&)lhs;
}
Thanks for your advices, here's a demo of the code working
I usually use a trick I learned from Robert Martin to do this.
I have a template class:
template <typename T>
class ComparisonOperators
{
protected:
~ComparisonOperators() {}
public:
friend bool operator==( T const& lhs, T const& rhs )
{
return lhs.compare( rhs ) == 0;
}
friend bool operator!=( T const& lhs, T const& rhs )
{
return lhs.compare( rhs ) != 0;
}
friend bool operator<( T const& lhs, T const& rhs )
{
return lhs.compare( rhs ) < 0;
}
friend bool operator<=( T const& lhs, T const& rhs )
{
return lhs.compare( rhs ) <= 0;
}
friend bool operator>( T const& lhs, T const& rhs )
{
return lhs.compare( rhs ) > 0;
}
friend bool operator>=( T const& lhs, T const& rhs )
{
return lhs.compare( rhs ) >= 0;
}
};
The class which needs the operators derives from this:
class Toto : public ComparisonOperators<Toto>
{
// ...
public:
// returns value < 0, == 0 or >0, according to
// whether this is <, == or > other.
int compare( Toto const& other ) const;
};
(My implementation is actually a bit more complicated, since it
uses some simple meta-programming to call isEqual, rather than
compare, if that function exists.)
EDIT:
And rereading your question: this is basically what you're doing, and it's pretty much the standard idiom for this sort of thing. I prefer using named functions like compare, but that is just a personal preference. The meta-programming trick to handle isEqual, however, is worth the bother: it means that you can use the same class for types which only support equality; you'll get an error when the compiler tries to instantiate e.g. operator<=, but the compiler won't try to instantiate it unless someone uses it. And it's often the case the isEqual can be implemented a lot more efficiently than compare.
EDIT 2:
For what it's worth: I do this systematically. I also have
ArithmeticOperators (defining e.g. + in terms of +=),
MixedTypeArithmeticOperators (like the above, but with two
types, T1, for which it is a base class, and T2; it
provides all of the combination of operators). and
STLIteratorOperators, which implements the STL iterator
interface based on something more rational and easier to
implement (basically, the GoF iterator with an isEqual
function). They saves a lot of boilerplate.
EDIT 3:
And finally: I just looked at the actual code in my toolkit.
Conditionally supporting isEqual is even simpler than
I remembered: the template class above has a public member:
bool isEqual( T const& other ) const
{
return static_cast< T const* >( this )->compare( other ) == 0;
}
And operator== and operator!= just use isEqual, no
template meta-programming involved. If the derived class
defines an isEqual, it hides this one, and it gets used. If
not, this one gets used.
friends are not inherited, so this idea won't work. However, you may cleverly use macro instead for example:
#define GEN(X) \
friend bool operator> (T const &lhs, T const &rhs) { return rhs < lhs; } \
friend bool operator==(T const &lhs, T const &rhs) { return !(lhs < rhs || lhs > rhs); } \
friend bool operator!=(T const &lhs, T const &rhs) { return !(rhs == lhs); } \
friend bool operator<=(T const &lhs, T const &rhs) { return !(rhs < lhs); } \
friend bool operator>=(T const &lhs, T const &rhs) { return !(lhs < rhs); }
And you may use as:
class Foo
{
...
GEN(foo)
};

why isn't my implicit ctor invoked in following

With the following template to try to make C++ 11/14's new class-enum work as-desired, I find that the following code doesn't even try to invoke the implicit ctor to use a nonmember template that would fit by VS2013 update 1:
BitTest is defined as:
template <typename enumT>
bool BitTest(const EnumeratedFlags<enumT>& lhs, const EnumeratedFlags<enumT>& rhs)
{
return (lhs & rhs);
}
testing code with anomalies:
enum class Flags { None = 0x00, CanChangeDataSources = 0x01, RequiresExclusiveAccess = 0x02 };
EnumeratedFlags<Flags> lhs(Flags::CanChangeDataSources);
// ... the following fails to even attempt to convert the Foo::Flags
if (BitTest(lhs, Flags::CanChangeDataSources)) { DoSomething(); }
// this compiles, but I don't know why it's necessary (and this is annoying and ugly)...
if (BitTest(lhs, EnumeratedFlags<Flags>(Flags::CanChangeDataSources))) { DoSomething(); }
Here is the template definition I am currently attempting to use:
template <typename enumT>
class EnumeratedFlags
{
public:
typedef enumT enum_type;
typedef typename std::underlying_type<enumT>::type store_type;
// constructors
EnumeratedFlags()
: m_bits(0)
{
}
EnumeratedFlags(enum_type flag)
: m_bits(static_cast<store_type>(flag))
{
}
explicit EnumeratedFlags(store_type value)
: m_bits(value)
{
}
EnumeratedFlags(const std::initializer_list<enum_type> & initializers)
: m_bits(0)
{
for (auto flag : initializers)
m_bits |= static_cast<store_type>(flag);
}
// operators
operator std::string () const
{
return to_string();
}
bool operator [] (enum_type flag) const
{
return test(flag);
}
store_type operator * () const
{
return m_bits;
}
operator bool () const
{
return m_bits != store_type(0);
}
// explicit accessors
store_type bits() const
{
return m_bits;
}
std::string to_string() const
{
std::string str(size(), '0');
for (size_t x = 0; x < size(); ++x)
str[size() - x - 1] = (m_bits & (1 << x) ? '1' : '0');
return str;
}
EnumeratedFlags & set(enum_type flag)
{
BitSet(m_bits, static_cast<store_type>(flag));
return *this;
}
EnumeratedFlags & set_if(enum_type flag, bool set_or_clear)
{
BitSetIf(m_bits, static_cast<store_type>(flag), set_or_clear);
return *this;
}
EnumeratedFlags & clear()
{
m_bits = store_type(0);
return *this;
}
EnumeratedFlags & flip()
{
m_bits = ~m_bits;
return *this;
}
EnumeratedFlags & flip(enum_type flag)
{
m_bits ^= static_cast<store_type>(flag);
return *this;
}
size_t count() const
{
// http://www-graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan
store_type bits = m_bits;
size_t total = 0;
for (; bits != 0; ++total)
{
bits &= bits - 1; // clear the least significant bit set
}
return total;
}
size_t size() const
{
// one character per possible bit
return sizeof(enum_type) * 8;
}
bool test(enum_type flag) const
{
return BitTest(m_bits, static_cast<store_type>(flag));
}
bool any() const
{
return m_bits != 0;
}
bool none() const
{
return m_bits == 0;
}
private:
store_type m_bits;
};
template <class charT, class traits, typename enumT>
std::basic_ostream<charT, traits> & operator << (std::basic_ostream<charT, traits> & os, const EnumeratedFlags<enumT> & flags)
{
return os << flags.to_string();
}
template <typename enumT>
EnumeratedFlags<enumT> operator & (const EnumeratedFlags<enumT>& lhs, const EnumeratedFlags<enumT>& rhs)
{
return EnumeratedFlags<enumT>(lhs.bits() & rhs.bits());
}
template <typename enumT>
EnumeratedFlags<enumT> operator | (const EnumeratedFlags<enumT>& lhs, const EnumeratedFlags<enumT>& rhs)
{
return EnumeratedFlags<enumT>(lhs.bits() | rhs.bits());
}
template <typename enumT>
EnumeratedFlags<enumT> operator ^ (const EnumeratedFlags<enumT>& lhs, const EnumeratedFlags<enumT>& rhs)
{
return EnumeratedFlags<enumT>(lhs.bits() ^ rhs.bits());
}
template <typename enumT>
bool BitTest(const EnumeratedFlags<enumT>& lhs, const EnumeratedFlags<enumT>& rhs)
{
return (lhs & rhs);
}
Basically, I would have thought that any free function in the form of X (const T & lhs, const T & rhs) would use up to one user-defined conversion to find a valid invocation of BitTest<>(), above. Instead, I have to explicitly state the conversion in my above code to get the compiler to use this function, which vastly reduces the expressive power of template class EnumeratedFlags<>.
In general, C++ drives me nuts that there isn't a good way to use bits that combines all of the features and good programming habits of using a scoped enum (enum class Foo) and bit-fields (or a similar named set of bits) and make using them very easy for the client-programmer while retaining basic sanity checking from the compiler (won't auto-convert to a numeric type or vice-verse). Seems like a more comprehensive improvement in the language spec is required to make a bit-field enum that really shines... or am I missing something?
From §14.8.1/6 [temp.arg.explicit]
Implicit conversions (Clause 4) will be performed on a function argument to convert it to the type of the corresponding function parameter if the parameter type contains no template-parameters that participate in template argument deduction.
In your example the second argument to BitTest() does participate in template argument deduction, hence no implicit conversion is attempted and the compiler is unable to convert the type Flags to const EnumeratedFlags<Flag>&.
You could solve it by converting the second parameter type to a non-deduced context, thus preventing it from participating in template argument deduction.
template <typename enumT>
bool BitTest(const EnumeratedFlags<enumT>& lhs,
const EnumeratedFlags<typename EnumeratedFlags<enumT>::enum_type>& rhs)
{
return (lhs & rhs);
}
Live demo
Of course, the other solution is to provide these overloads instead
template <typename enumT>
bool BitTest(const EnumeratedFlags<enumT>& lhs,
enumT rhs)
{
return (lhs & EnumeratedFlags<enumT>(rhs));
}
template <typename enumT>
bool BitTest(enumT lhs,
const EnumeratedFlags<enumT>& rhs)
{
return BitTest(rhs, lhs);
}
My question is already answered above. However, this has been bothering me forever, so I was able to leverage everyone's responses and came up with, what I think is for the first time, a mechanism that I really like!
This gives me a way to use C++ enumerations to store bit flag values, and then to logically bit-manipulate them at will, while never losing type information or the compiler's assistance with making sure that I fall into the pit of success. :)
I hope this helps you too!
(Note: this code compiles and runs properly under VS2013 update 1)
#pragma once
#include <type_traits>
// This is my ongoing attempt to make a really solid enumeration facility in C++11/14
//
// What I hate about C++98 (and older) enum
// - lack of namespace scoping of the non-class enum (name collisions everywhere)
// - auto conversion to numeric types (int i = MyEnumConstant), but no conversion back again (so supports losing info, but restricts regaining it)
//
// What I hate about C++11/14 built-in `enum class X`
// - having to constantly cast in order to treat them (now neither direction works)
// - no built-in mechanism to treat enumerated values as bits or bit-flags
template <typename enum_type>
class bitflag_enum
{
public:
// expose our underlying types
typedef enum_type enum_type;
typedef typename std::underlying_type<enum_type>::type store_type;
// constructors
bitflag_enum()
: m_bits(0)
{
}
bitflag_enum(enum_type flag)
: m_bits(static_cast<store_type>(flag))
{
}
explicit bitflag_enum(store_type value)
: m_bits(value)
{
}
// operators
operator bool() const
{
return m_bits != store_type(0);
}
// explicit accessors
store_type bits() const
{
return m_bits;
}
private:
store_type m_bits;
};
// because implicit conversion isn't considered if a type participates in template type deduction,
// we've defined both homogeneous and heterogeneous operators here for bitflag_enum<enum_type> and enum_type
// hence we define logical operators &, |, ^ and comparisons for TxT, TxU, UxT (where T is bitflag_enum<enum_type>, and U is enum_type)
template <typename enum_type>
bool operator != (bitflag_enum<enum_type> lhs, bitflag_enum<enum_type> rhs)
{
return bitflag_enum<enum_type>(lhs.bits() != rhs.bits());
}
template <typename enum_type>
bool operator != (bitflag_enum<enum_type> lhs, enum_type rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(lhs.bits() != static_cast<store_type>(rhs));
}
template <typename enum_type>
bool operator != (enum_type lhs, bitflag_enum<enum_type> rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(static_cast<store_type>(lhs) != rhs.bits());
}
template <typename enum_type>
bool operator == (bitflag_enum<enum_type> lhs, bitflag_enum<enum_type> rhs)
{
return bitflag_enum<enum_type>(lhs.bits() == rhs.bits());
}
template <typename enum_type>
bool operator == (bitflag_enum<enum_type> lhs, enum_type rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(lhs.bits() == static_cast<store_type>(rhs));
}
template <typename enum_type>
bool operator == (enum_type lhs, bitflag_enum<enum_type> rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(static_cast<store_type>(lhs) == rhs.bits());
}
template <typename enum_type>
bitflag_enum<enum_type> operator & (bitflag_enum<enum_type> lhs, bitflag_enum<enum_type> rhs)
{
return bitflag_enum<enum_type>(lhs.bits() & rhs.bits());
}
template <typename enum_type>
bitflag_enum<enum_type> operator & (bitflag_enum<enum_type> lhs, enum_type rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(lhs.bits() & static_cast<store_type>(rhs));
}
template <typename enum_type>
bitflag_enum<enum_type> operator & (enum_type lhs, bitflag_enum<enum_type> rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(static_cast<store_type>(lhs)& rhs.bits());
}
template <typename enum_type>
bitflag_enum<enum_type> operator | (bitflag_enum<enum_type> lhs, bitflag_enum<enum_type> rhs)
{
return bitflag_enum<enum_type>(lhs.bits() | rhs.bits());
}
template <typename enum_type>
bitflag_enum<enum_type> operator | (bitflag_enum<enum_type> lhs, enum_type rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(lhs.bits() | static_cast<store_type>(rhs));
}
template <typename enum_type>
bitflag_enum<enum_type> operator | (enum_type lhs, bitflag_enum<enum_type> rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(static_cast<store_type>(lhs) | rhs.bits());
}
template <typename enum_type>
bitflag_enum<enum_type> operator ^ (bitflag_enum<enum_type> lhs, bitflag_enum<enum_type> rhs)
{
return bitflag_enum<enum_type>(lhs.bits() ^ rhs.bits());
}
template <typename enum_type>
bitflag_enum<enum_type> operator ^ (bitflag_enum<enum_type> lhs, enum_type rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(lhs.bits() ^ static_cast<store_type>(rhs));
}
template <typename enum_type>
bitflag_enum<enum_type> operator ^ (enum_type lhs, bitflag_enum<enum_type> rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(static_cast<store_type>(lhs) ^ rhs.bits());
}
// The only missing pieces above are for the UxU cases
// we allow you to have those by defining a specialization of is_bitflag_enum<>, as follows:
//
// template <> struct is_bitflag_enum<YourEnumType> : std::true_type { };
//
// However, by default, no other types will convert to an bitflag_enum<> unless you explicitly say you want it
//
// If you have asked for them, then you can use MyEnum::ValueX | MyEnum::ValueY and that will produce a bitflag_enum<MyEnum>
// so your code can simply use your enumeration values with scope and as-if they were bit flags as you would think you could
// don't mess up existing enumerations or types by defining these global operators on every existing type!
template <typename enum_type> struct is_bitflag_enum : std::false_type { };
template <typename enum_type>
typename std::enable_if<is_bitflag_enum<enum_type>::value, bitflag_enum<enum_type>>::type
operator & (enum_type lhs, enum_type rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(static_cast<store_type>(lhs) & static_cast<store_type>(rhs));
}
template <typename enum_type>
typename std::enable_if<is_bitflag_enum<enum_type>::value, bitflag_enum<enum_type>>::type
operator | (enum_type lhs, enum_type rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(static_cast<store_type>(lhs) | static_cast<store_type>(rhs));
}
template <typename enum_type>
typename std::enable_if<is_bitflag_enum<enum_type>::value, bitflag_enum<enum_type>>::type
operator ^ (enum_type lhs, enum_type rhs)
{
using store_type = std::underlying_type<enum_type>::type;
return bitflag_enum<enum_type>(static_cast<store_type>(lhs) ^ static_cast<store_type>(rhs));
}

Relational operators on a class template

This will not work
template<typename T>
struct foo {
T t;
};
bool operator==(const foo &lhs, const foo &rhs) { //error, requires template arg
return lhs.t == rhs.t;
}
Is this the correct way to solve this? I want define also the operators <,>,<=,>=,!= so doing template<typename T> on all of them would be lengthy.
template<typename T>
struct foo {
T t;
};
template<typename T>
bool operator==(const foo<T> &lhs, const foo<T> &rhs) {
return lhs.t == rhs.t;
}
There are two solutions: you can define them as const member functions inside the class
template<typename T>
struct foo {
T t;
bool operator==(const foo &lhs, const foo &rhs) const { return lhs.t == rhs.t; }
// same for the other relational operators
};
This works because inside the class you can use foo as a shorthand for foo<T>.
An alternative is to define them as friend non-member functions inside the class
template<typename T>
class foo {
T t;
friend bool operator==(const foo &lhs, const foo &rhs) const { return lhs.t == rhs.t; }
// same for the other relational operators
};
If you define t as a private member, then you actually need to make operator== a friend function in order to let it gain access. Note however, that this will have the side-effect as injecting them as non-member non-template functions in the surrounding namespace. This has some consequences for argument-dependent name lookup.
if you don't care about implicit conversions, you can set them as member functions, so you won't have to retype it each time.
but if you have to define them as free functions, I'm afraid you don't have a choice.

Relational Operator Implementation Dilemma

I'm in the process of designing several classes that need to support operators !=, >, <=, and >=. These operators will be implemented in terms of operators == and <.
At this stage, I need to make a choice between inheritance¹ and forcing my consumers to use std::rel_ops² "manually".
[1] Inheritance (possible implementation):
template<class T> class RelationalOperatorsImpl
{
protected:
RelationalOperatorsImpl() {}
~RelationalOperatorsImpl() {}
friend bool operator!=(const T& lhs, const T& rhs) {return !(lhs == rhs);}
friend bool operator>(const T& lhs, const T& rhs) {return (rhs < lhs);}
friend bool operator<=(const T& lhs, const T& rhs) {return !(rhs < lhs);}
friend bool operator>=(const T& lhs, const T& rhs) {return !(lhs < rhs);}
};
template<typename T> class Foo : RelationalOperatorsImpl< Foo<T> >
{
public:
explicit Foo(const T& value) : m_Value(value) {}
friend bool operator==(const Foo& lhs, const Foo& rhs) {return (lhs.m_Value == rhs.m_Value);}
friend bool operator<(const Foo& lhs, const Foo& rhs) {return (lhs.m_Value < rhs.m_Value);}
private:
T m_Value;
};
[2] std::rel_ops glue:
template<typename T> class Foo
{
public:
explicit Foo(const T& value) : m_Value(value) {}
friend bool operator==(const Foo& lhs, const Foo& rhs) {return (lhs.m_Value == rhs.m_Value);}
friend bool operator<(const Foo& lhs, const Foo& rhs) {return (lhs.m_Value < rhs.m_Value);}
private:
T m_Value;
};
void Consumer()
{
using namespace std::rel_ops;
//Operators !=, >, >=, and <= will be instantiated for Foo<T> (in this case) on demand.
}
I'm basically trying to avoid code repetition. Any thoughts as to which method "feels" better?
Have you considered using boost, and having your class inherit from boost::less_than_comparable<T> and boost::equality_comparable<T>? It is akin to your first suggestion, with some pros and cons. Pros: avoids code duplication; Cons: creates a dependency on boost.
Since boost is a very common C++ library (if you don't use it already, you should seriously consider start using it), the con factor is dimmed.
I think std::rel_ops is quite nice, but there's one thing to consider first: std::rel_ops provides operators as template functions that accept two parameters of the same type. Because most conversions (including e.g. arithmetic promotions and user-defined conversions) are not performed when template argument deduction occurs, this means that you would not be able to use any of these additional operators (e.g. !=) with such conversions.
E.g. if you have a class MyInt that attempts to behave like a regular integer, you might have written conversion functions/constructors or templated operators so that you can do
MyInt x, y;
x < 5;
9 == x;
However,
x > 5;
30 <= x;
won't work (with std::rel_ops) because the two arguments are of different types, so template argument deduction will fail.