I've been meaning to implement total ordering via SFINAE and the Curiously Recurring Template Pattern idiom for a while now. The general idea is as follows:
<, >, etc.)For simplicity, I have ignored the == and != operators in this example.
Relational Operator Detection
I first define classes to statically check whether the class defines a specific feature. For example, here I detect the presence of the less-than operator, or operator<.
template <typename T>
class has_less
{
protected:
template <typename C> static char &test(decltype(std::declval<C>() < std::declval<C>()));
template <typename C> static long &test(...);
public:
enum {
value = sizeof(test<T>(0)) == sizeof(char)
};
};
template <typename T>
constexpr bool has_less_v = has_less<T>::value;
Total Ordering
I then define classes that implement total ordering from a given operator, for example, to define total ordering from the less-than operator, I would use the following:
template <typename T>
struct less_than_total
{
bool operator>(const T &t) { return t < static_cast<T&>(*this); }
bool operator>=(const T &t) { return !(static_cast<T&>(*this) < t); }
bool operator<=(const T &t) { return !(t < static_cast<T&>(*this)); }
};
Base Class
I then define a single base class that creates a typedef to implement the remaining operators by detecting the implemented operator.
template <bool B, typename T, typename F>
using conditional_t = typename std::conditional<B, T, F>::type;
template <typename T>
using total_ordering = conditional_t< // has_less
has_less_v<T>,
less_than_total<T>,
conditional_t< // has_less_equal
has_less_equal_v<T>,
less_equal_total<T>,
conditional_t< // has_greater
has_greater_v<T>,
greater_total<T>,
conditional_t< // has_greater_equal
has_greater_equal_v<T>,
greater_equal_total<T>,
symmetric<T> // symmetry
> // has_greater_equal
> // has_greater
> // has_less_equal
>; // has_less
Inheritance
All of these steps, individually, work. However, when I actually inherit from the base class using the curiously recurring pattern, the resulting class implements only one of these operators and the detection algorithms fail.
Example
I've boiled down the issue to a minimal example consisting of the core parts: operator detection (has_less, has_greater), total ordering implementation (total), a simplified base class (total), and a simple struct using these relational operators (A).
#include <type_traits>
// DETECTION
template <typename T>
class has_less
{
protected:
template <typename C> static char &test(decltype(std::declval<C>() < std::declval<C>()));
template <typename C> static long &test(...);
public:
enum {
value = sizeof(test<T>(0)) == sizeof(char)
};
};
template <typename T>
class has_greater
{
protected:
template <typename C> static char &test(decltype(std::declval<C>() > std::declval<C>()));
template <typename C> static long &test(...);
public:
enum {
value = sizeof(test<T>(0)) == sizeof(char)
};
};
// TOTAL ORDERING
template <typename T>
struct less_than_total
{
bool operator>(const T &t) { return t < static_cast<T&>(*this); }
bool operator>=(const T &t) { return !(static_cast<T&>(*this) < t); }
bool operator<=(const T &t) { return !(t < static_cast<T&>(*this)); }
};
template <typename T>
struct symmetry
{};
template <bool B, typename T, typename F>
using conditional_t = typename std::conditional<B, T, F>::type;
template <typename T>
struct total: conditional_t<
has_less<T>::value,
less_than_total<T>,
symmetry<T>
>
{};
// TEST
struct A: total<A>
{
bool operator<(const A &r)
{
return true;
}
};
int main(void)
{
static_assert(has_less<A>::value, "");
static_assert(has_greater<A>::value, "");
return 0;
}
Ideally, this example would compile, however, I get:
$ clang++ a.cpp -o a -std=c++14
a.cpp:79:5: error: static_assert failed ""
static_assert(has_less<A>::value, "");
^ ~~~~~~~~~~~~~~~~~~
a.cpp:80:5: error: static_assert failed ""
static_assert(has_greater<A>::value, "");
Unfortunately, the base class is not detecting the operators during inheritance, and SFINAE does not detect the less than or greater than operators in the resulting class.
Question and Follow-Up
I would like to know why this fails, since I have been doing member-function detection and member-type detection for a long time with the curiously recurring pattern without problem. And assuming there is no direct issue with my code, are there any work-arounds to implement total ordering in such a manner?
Edit
I'm able to achieve a subset of what I want using std::enable_if. In this case, the only simple answer is to implement everything in terms of operator< and then implement total ordering from that operator.
template <typename T>
struct total
{
template <typename U = T>
typename std::enable_if<has_less<U>::value, bool>::type
bool operator>(const T &l, const T &r) { return r < t; }
};
If would still like an answer for why my operator detection via SFINAE fails during inheritance, but succeeds for inherited methods.
The main problem with this is that A is an incomplete type when has_less<A> is instantiated (during the instantiation of total<A> as A's base class) -- at this point, the compiler doesn't yet know that A has an operator <.
So, has_less<A> is instantiated with its value == 0, and symmetry<A> is selected for total<A>'s base class -- so A never gets any of its additional operators.
After all this is decided, the compiler sees the definition of A::operator <, which it adds to A. After this, A is complete.
So we know why static_assert(has_greater<A>::value, ""); fails, but shouldn't we expect static_assert(has_less<A>::value, ""); to succeed? After all, now A has a less-than operator. The thing is, has_less<A> was already instantiated with the incomplete A, with value == 0 -- even though A has changed, there is no mechanism for updating previously instantiated compile-time values. So this assertion fails too, even though it looks like it should succeed.
To show that this is the case, make a copy of has_less, name it has_less2, and change the static assertion to static_assert(has_less2<A>::value, "");. Because has_less2<A> is instantiated after A gets its less-than operator, this assertion succeeds.
One way you can make the code succeed is to forward-declare A and declare a global operator <, for comparing two A objects, so that the compiler knows about this operator before A's base class is worked out. Something like this:
struct A;
bool operator < (const A &lh, const A& rh);
struct A : total<A> {
friend bool operator < (const A &lh, const A& rh) {
return true;
}
};
I understand that this is not really what you want, however -- it would be much nicer if the CRTP setup happened automatically, without any special accommodation required in the derived class. But this might still give you some insight which can help lead you to a proper solution. I will also think about this some more, and if I come up with anything, I'll update this answer.
One more thing: Comparison member functions should be const qualified. Like
bool operator>(const T &t) const { ...
This is really important and will prevent a lot of non-obvious problems compiling code which uses these classes later on.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With