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).
Related
Consider the following code snippet :
[[nodiscard]] bool operator==(const BasicIterator<const Type>& rhs) const noexcept {
if( this == &rhs ) {
return true;
}
return node_ == rhs.node_;
}
[[nodiscard]] bool operator==(const BasicIterator<Type>& rhs) const noexcept {
// how to call existing operator== implementation ??
//return operator==( rhs );
}
How should I use the same implementation for both operator==? Is it possible to call operator== <const Type> version from operator== <Type>?
Is there any cast for template types in this case?
Is Type a template? If not, maybe make it a template?
template <typename T>
[[nodiscard]] bool operator==(const BasicIterator<T>& rhs) const noexcept {
if( this == &rhs ) {
return true;
}
return node_ == rhs.node_;
}
If you do not want to expose this template to users, hide it somewhere and use in implementation:
private:
template <typename T>
[[nodiscard]] bool operator==(const BasicIterator<T>& lhs, const BasicIterator<T>& rhs) const noexcept {
if( &lhs== &rhs ) {
return true;
}
return lhs.node_ == rhs.node_;
}
public:
[[nodiscard]] bool operator==(const BasicIterator<const Type>& rhs) const noexcept {
return operator==<const Type>(*this, rhs);
}
[[nodiscard]] bool operator==(const BasicIterator<Type>& rhs) const noexcept {
return operator==<Type>(*this, rhs);
}
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.
Why std::optional (std::experimental::optional in libc++ at the moment) does not have specialization for reference types (compared with boost::optional)?
I think it would be very useful option.
Is there some object with reference to maybe already existing object semantics in STL?
When n3406 (revision #2 of the proposal) was discussed, some committee members were uncomfortable with optional references. In n3527 (revision #3), the authors decided to make optional references an auxiliary proposal, to increase the chances of getting optional values approved and put into what became C++14. While optional didn't quite make it into C++14 for various other reasons, the committee did not reject optional references and is free to add optional references in the future should someone propose it.
The main problem with std::optional <T&> is — what should optRef = obj do in the following case:
optional<T&> optRef;
…;
T obj {…};
optRef = obj; // <-- here!
Variants:
Always rebind — (&optRef)->~optional(); new (&optRef) optional<T&>(obj).
Assign through — *optRef = obj (UB when !optRef before).
Bind if empty, assign through otherwise — if (optRef) {do1;} else {do2;}.
No assignment operator — compile-time error "trying to use a deleted operator".
Pros of every variant:
Always rebind (chosen by boost::optional and n1878):
Consistency between the cases when !optRef and optRef.has_value() — post-condition &*optRef == &obj is always met.
Consistency with usual optional<T> in the following aspect: for usual optional<T>, if T::operator= is defined to act as destroying and constructing (and some argue that it must be nothing more than optimization for destroying-and-constructing), opt = … de facto acts similarly like (&opt)->~optional(); new (&opt) optional<T&>(obj).
Assign through:
Consistency with pure T& in the following aspect: for pure T&, ref = … assigns through (not rebinds the ref).
Consistency with usual optional<T> in the following aspect: for usual optional<T>, when opt.has_value(), opt = … is required to assign through, not to destroy-and-construct (see template <class U> optional<T>& optional<T>::operator=(U&& v) in n3672 and on cppreference.com).
Consistency with usual optional<T> in the following aspect: both haveoperator= defined at least somehow.
Bind if empty, assign through otherwise — I see no real benefits, IMHO this variant arises only when proponents of #1 argue with proponents of #2, however formally it's even more consistent with the letter of requirements for template <class U> optional<T>& optional<T>::operator=(U&& v) (but not with the spirit, IMHO).
No assignment operator (chosen by n3406):
Consistency with pure T& in the following aspect: pure T& doesn't allow to rebind itself.
No ambiguous behavior.
See also:
Let’s Talk about std::optional<T&> and optional references.
Why Optional References Didn’t Make It In C++17.
There is indeed something that has reference to maybe existing object semantics. It is called a (const) pointer. A plain old non-owning pointer. There are three differences between references and pointers:
Pointers can be null, references can not. This is exactly the difference you want to circumvent with std::optional.
Pointers can be redirected to point to something else. Make it const, and that difference disappears as well.
References need not be dereferenced by -> or *. This is pure syntactic sugar and possible because of 1. And the pointer syntax (dereferencing and convertible to bool) is exactly what std::optional provides for accessing the value and testing its presence.
Update:
optional is a container for values. Like other containers (vector, for example) it is not designed to contain references. If you want an optional reference, use a pointer, or if you indeed need an interface with a similar syntax to std::optional, create a small (and trivial) wrapper for pointers.
Update2: As for the question why there is no such specialization: because the committee simply did opt it out. The rationale might be found somewhere in the papers. It possibly is because they considered pointers to be sufficient.
IMHO it is very okay to make std::optional<T&> available. However there is a subtle issue about templates. Template parameters can become tricky to deal with if there are references.
Just as the way we solved the problem of references in template parameters, we can use a std::reference_wrapper to circumvent the absence of std::optional<T&>. So now it becomes std::optional<std::reference_wrapper<T>>. However I recommend against this use because 1) it is way too verbose to both write the signature (trailing return type saves us a bit) and the use of it (we have to call std::reference_wrapper<T>::get() to get the real reference), and 2) most programmers have already been tortured by pointers so that it is like an instinctive reaction that when they receive a pointer they test first whether it is null so it is not quite much an issue now.
If I would hazard a guess, it would be because of this sentence in the specification of std::experimental::optional. (Section 5.2, p1)
A program that necessitates the instantiation of template optional
for a reference type, or for possibly cv-qualified types in_place_t or
nullopt_t is ill-formed.
I stumbled upon this several times and I finally decided to implement my solution that doesn't depend on boost. For reference types it disables assignment operator and doesn't allow for comparison of pointers or r-values. It is based on a similar work I did some time ago, and it uses nullptr instead of nullopt to signal absence of value. For this reason, the type is called nullable and compilation is disabled for pointer types (they have nullptr anyway). Please let me know if you find any obvious or any non-obvious problem with it.
#ifndef COMMON_NULLABLE_H
#define COMMON_NULLABLE_H
#pragma once
#include <cstddef>
#include <stdexcept>
#include <type_traits>
namespace COMMON_NAMESPACE
{
class bad_nullable_access : public std::runtime_error
{
public:
bad_nullable_access()
: std::runtime_error("nullable object doesn't have a value") { }
};
/**
* Alternative to std::optional that supports reference (but not pointer) types
*/
template <typename T, typename = std::enable_if_t<!std::is_pointer<T>::value>>
class nullable final
{
public:
nullable()
: m_hasValue(false), m_value{ } { }
nullable(T value)
: m_hasValue(true), m_value(std::move(value)) { }
nullable(std::nullptr_t)
: m_hasValue(false), m_value{ } { }
nullable(const nullable& value) = default;
nullable& operator=(const nullable& value) = default;
nullable& operator=(T value)
{
m_hasValue = true;
m_value = std::move(value);
return *this;
}
nullable& operator=(std::nullptr_t)
{
m_hasValue = false;
m_value = { };
return *this;
}
const T& value() const
{
if (!m_hasValue)
throw bad_nullable_access();
return m_value;
}
T& value()
{
if (!m_hasValue)
throw bad_nullable_access();
return m_value;
}
bool has_value() const { return m_hasValue; }
const T* operator->() const { return &m_value; }
T* operator->() { return &m_value; }
const T& operator*() const { return m_value; }
T& operator*() { return m_value; }
public:
template <typename T2>
friend bool operator==(const nullable<T2>& lhs, const nullable<T2>& rhs);
template <typename T2>
friend bool operator!=(const nullable<T2>& lhs, const nullable<T2>& rhs);
template <typename T2>
friend bool operator==(const nullable<std::decay_t<T2>>& lhs, const nullable<T2&>& rhs);
template <typename T2>
friend bool operator!=(const nullable<std::decay_t<T2>>& lhs, const nullable<T2&>& rhs);
template <typename T2>
friend bool operator==(const nullable<T2&>& lhs, const nullable<std::decay_t<T2>>& rhs);
template <typename T2>
friend bool operator!=(const nullable<T2&>& lhs, const nullable<std::decay_t<T2>>& rhs);
template <typename T2>
friend bool operator==(const nullable<T2>& lhs, const T2& rhs);
template <typename T2>
friend bool operator==(const T2& lhs, const nullable<T2>& rhs);
template <typename T2>
friend bool operator==(const nullable<T2>& lhs, std::nullptr_t);
template <typename T2>
friend bool operator!=(const nullable<T2>& lhs, const T2& rhs);
template <typename T2>
friend bool operator!=(const T2& lhs, const nullable<T2>& rhs);
template <typename T2>
friend bool operator==(std::nullptr_t, const nullable<T2>& rhs);
template <typename T2>
friend bool operator!=(const nullable<T2>& lhs, std::nullptr_t);
template <typename T2>
friend bool operator!=(std::nullptr_t, const nullable<T2>& rhs);
private:
bool m_hasValue;
T m_value;
};
// Template spacialization for references
template <typename T>
class nullable<T&> final
{
public:
nullable()
: m_hasValue(false), m_value{ } { }
nullable(T& value)
: m_hasValue(true), m_value(&value) { }
nullable(std::nullptr_t)
: m_hasValue(false), m_value{ } { }
nullable(const nullable& value) = default;
nullable& operator=(const nullable& value) = default;
const T& value() const
{
if (!m_hasValue)
throw bad_nullable_access();
return *m_value;
}
T& value()
{
if (!m_hasValue)
throw bad_nullable_access();
return *m_value;
}
bool has_value() const { return m_hasValue; }
const T* operator->() const { return m_value; }
T* operator->() { return m_value; }
const T& operator*() const { return *m_value; }
T& operator*() { return *m_value; }
public:
template <typename T2>
friend bool operator==(const nullable<std::decay_t<T2>>& lhs, const nullable<T2&>& rhs);
template <typename T2>
friend bool operator!=(const nullable<std::decay_t<T2>>& lhs, const nullable<T2&>& rhs);
template <typename T2>
friend bool operator==(const nullable<T2&>& lhs, const nullable<std::decay_t<T2>>& rhs);
template <typename T2>
friend bool operator!=(const nullable<T2&>& lhs, const nullable<std::decay_t<T2>>& rhs);
template <typename T2>
friend bool operator==(const nullable<T2&>& lhs, const nullable<T2&>& rhs);
template <typename T2>
friend bool operator!=(const nullable<T2&>& lhs, const nullable<T2&>& rhs);
template <typename T2>
friend bool operator==(const nullable<T2&>& lhs, const std::decay_t<T2>& rhs);
template <typename T2>
friend bool operator!=(const nullable<T2&>& lhs, const std::decay_t<T2>& rhs);
template <typename T2>
friend bool operator==(const std::decay_t<T2>& lhs, const nullable<T2&>& rhs);
template <typename T2>
friend bool operator!=(const std::decay_t<T2>& lhs, const nullable<T2&>& rhs);
template <typename T2>
friend bool operator==(const nullable<T2>& lhs, std::nullptr_t);
template <typename T2>
friend bool operator==(std::nullptr_t, const nullable<T2>& rhs);
template <typename T2>
friend bool operator!=(const nullable<T2>& lhs, std::nullptr_t);
template <typename T2>
friend bool operator!=(std::nullptr_t, const nullable<T2>& rhs);
private:
bool m_hasValue;
T* m_value;
};
template <typename T>
using nullableref = nullable<T&>;
template <typename T2>
bool operator==(const nullable<T2>& lhs, const nullable<T2>& rhs)
{
if (lhs.m_hasValue != rhs.m_hasValue)
return false;
if (lhs.m_hasValue)
return lhs.m_value == rhs.m_value;
else
return true;
}
template <typename T2>
bool operator!=(const nullable<T2>& lhs, const nullable<T2>& rhs)
{
if (lhs.m_hasValue != rhs.m_hasValue)
return true;
if (lhs.m_hasValue)
return lhs.m_value != rhs.m_value;
else
return false;
}
template <typename T2>
bool operator==(const nullable<std::decay_t<T2>>& lhs, const nullable<T2&>& rhs)
{
if (lhs.m_hasValue != rhs.m_hasValue)
return true;
if (lhs.m_hasValue)
return lhs.m_value != *rhs.m_value;
else
return false;
}
template <typename T2>
bool operator!=(const nullable<std::decay_t<T2>>& lhs, const nullable<T2&>& rhs)
{
if (lhs.m_hasValue != rhs.m_hasValue)
return true;
if (lhs.m_hasValue)
return lhs.m_value != *rhs.m_value;
else
return false;
}
template <typename T2>
bool operator==(const nullable<T2&>& lhs, const nullable<std::decay_t<T2>>& rhs)
{
if (lhs.m_hasValue != rhs.m_hasValue)
return false;
if (lhs.m_hasValue)
return *lhs.m_value == rhs.m_value;
else
return true;
}
template <typename T2>
bool operator!=(const nullable<T2&>& lhs, const nullable<std::decay_t<T2>>& rhs)
{
if (lhs.m_hasValue != rhs.m_hasValue)
return true;
if (lhs.m_hasValue)
return *lhs.m_value != rhs.m_value;
else
return false;
}
template <typename T2>
bool operator==(const nullable<T2&>& lhs, const nullable<T2&>& rhs)
{
if (lhs.m_hasValue != rhs.m_hasValue)
return false;
if (lhs.m_hasValue)
return *lhs.m_value == *rhs.m_value;
else
return true;
}
template <typename T2>
bool operator!=(const nullable<T2&>& lhs, const nullable<T2&>& rhs)
{
if (lhs.m_hasValue != rhs.m_hasValue)
return true;
if (lhs.m_hasValue)
return *lhs.m_value != *rhs.m_value;
else
return false;
}
template <typename T2>
bool operator==(const nullable<T2&>& lhs, const std::decay_t<T2>& rhs)
{
if (!lhs.m_hasValue)
return false;
return *lhs.m_value == rhs;
}
template <typename T2>
bool operator!=(const nullable<T2&>& lhs, const std::decay_t<T2>& rhs)
{
if (!lhs.m_hasValue)
return true;
return *lhs.m_value != rhs;
}
template <typename T2>
bool operator==(const std::decay_t<T2>& lhs, const nullable<T2&>& rhs)
{
if (!rhs.m_hasValue)
return false;
return lhs == *rhs.m_value;
}
template <typename T2>
bool operator!=(const std::decay_t<T2>& lhs, const nullable<T2&>& rhs)
{
if (!rhs.m_hasValue)
return true;
return lhs != *rhs.m_value;
}
template <typename T2>
bool operator==(const nullable<T2>& lhs, const T2& rhs)
{
if (!lhs.m_hasValue)
return false;
return lhs.m_value == rhs;
}
template <typename T2>
bool operator!=(const nullable<T2>& lhs, const T2& rhs)
{
if (!lhs.m_hasValue)
return true;
return lhs.m_value != rhs;
}
template <typename T2>
bool operator==(const T2& lhs, const nullable<T2>& rhs)
{
if (!rhs.m_hasValue)
return false;
return lhs == rhs.m_value;
}
template <typename T2>
bool operator!=(const T2& lhs, const nullable<T2>& rhs)
{
if (!rhs.m_hasValue)
return true;
return lhs != rhs.m_value;
}
template <typename T2>
bool operator==(const nullable<T2>& lhs, std::nullptr_t)
{
return !lhs.m_hasValue;
}
template <typename T2>
bool operator!=(const nullable<T2>& lhs, std::nullptr_t)
{
return lhs.m_hasValue;
}
template <typename T2>
bool operator==(std::nullptr_t, const nullable<T2>& rhs)
{
return !rhs.m_hasValue;
}
template <typename T2>
bool operator!=(std::nullptr_t, const nullable<T2>& rhs)
{
return rhs.m_hasValue;
}
}
#endif // COMMON_NULLABLE_H
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)
};
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.