27

So I want to write an automatic !=:

template<typename U, typename T>
bool operator!=(U&& u, T&& t) {
  return !( std::forward<U>(u) == std::forward<T>(t) );
}

but that is impolite1. So I write

// T() == U() is valid?
template<typename T, typename U, typename=void>
struct can_equal:std::false_type {};

template<typename T, typename U>
struct can_equal<
   T,
   U,
   typename std::enable_if<
      std::is_convertible<
         decltype( std::declval<T>() == std::declval<U>() ),
         bool
      >::value
   >::type
>: std::true_type {};

which is a type traits class that says "is t == u valid code that returns a type convertible to bool".

So I improve my !=:

template<typename U, typename T,
  typename=typename std::enable_if<can_equal<T,U>::value>::type
>
bool operator!=(U&& u, T&& t) {
  return !( std::forward<U>(u) == std::forward<T>(t) );
}

and now it only is a valid override if == exists. Sadly, it is a bit greedy:

struct test {
};
bool operator==(const test&, const test&);
bool operator!=(const test&, const test&);

as it will snarf up pretty much every test() != test() rather than the above != being called. I think this is not desired -- I would rather call an explicit != than auto-forward to == and negate.

So, I write up this traits class:

template<typename T, typename U,typename=void>
struct can_not_equal // ... basically the same as can_equal, omitted

which tests if T != U is valid.

We then augment the != as follows:

template<typename U, typename T,
  typename=typename std::enable_if<
    can_equal<T,U>::value
    && !can_not_equal<T,U>::value
  >::type
>
bool operator!=(U&& u, T&& t) {
  return !( std::forward<U>(u) == std::forward<T>(t) );
}

which, if you parse it, says "this sentence is false" -- operator!= exists between T and U iff operator!= does not exist between T and U.

Not surprisingly, every compiler I have tested segfaults when fed this. (clang 3.2, gcc 4.8 4.7.2 intel 13.0.1). I suspect that what I'm doing is illegal, but I would love to see the standard reference. (edit: What I'm doing is illegal, because it induces an unbounded recursive template expansion, as determining if my != applies requires that we check if my != applies. The version linked in the comments, with #if 1, gives a sensible error).

But my question: is there a way I can convince my SFINAE based override to ignore "itself" when deciding if it should fail or not, or somehow get rid of the self referential issue somehow? Or lower the precedence of my operator!= low enough so any explicit != wins out, even if it is otherwise not as good a match?

The one that doesn't check for "!= does not exist" works reasonably well, but not well enough for me to be as impolite as to inject it into the global namespace.

The goal is any code that would compile without my "magic" != does exactly the same thing once my "magic" != is introduced. If and only if != is otherwise invalid and bool r = !(a==b) is well formed should my "magic" != kick in.


Footnote 1: If you create a template<typename U, typename T> bool operator!=(U&& u, T&& t), SFINAE will think that every pair of types has a valid != between them. Then when you try to actually call !=, it is instantiated, and fails to compile. On top of that, you stomp on bool operator!=( const foo&, const foo& ) functions, because you are a better match for foo() != foo() and foo a, b; a != b;. I consider doing both of these impolite.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I’m sure you’re aware, but `namespace std::rel_ops` from `` has the practical (and naïve) version of this. – Jon Purdy Apr 09 '13 at 21:02
  • 2
    Swap the default template argument for a non-type parameter and please supply an SSCCE somewhere that can be easily copied. – Xeo Apr 09 '13 at 21:03
  • 1
    +1 just for doing crazy stuff. Recursive SFINAE... Wow :-P – Daniel Frey Apr 09 '13 at 21:27
  • 1
    Now we need the TMP equivalent of Bertrand Russel – Andy Prowl Apr 09 '13 at 21:30
  • @Xeo [SSCCE example](http://liveworkspace.org/code/3kW04t$13), with non-type SFINAE for bonus, and some test code. This does answer why the segfault -- template recursion depth limit (not sure why the type version segfaulted before detecting it, maybe the memoizer blew up?) – Yakk - Adam Nevraumont Apr 09 '13 at 21:33
  • @JonPurdy `std::rel_ops` does the first, impolite, version -- which introduces a `x!=y` override regardless of if it is valid or not! I'm trying to SFINAE detect if my `!=` is the best match. It does point out that maybe making my `!=` a `const&` only operator might be a way to lower the precedence of it low enough not to cause problems... – Yakk - Adam Nevraumont Apr 09 '13 at 21:38
  • I think that, in any case, the better way to go about this would be to explicitly mark the type to get automatic `operator!=`. Either inherit from `equality_comparable` or something, or specialize `is_equality_comparable`, and check that. (Also, I think the word you're looking for is "tautological".) – Xeo Apr 09 '13 at 21:52
  • @Xeo: A tautology is something which is true in all cases, while this condition is simply self-referential (an absurdum) - maybe you meant "autological"? – Andy Prowl Apr 09 '13 at 21:55
  • @Andy: Yeah, I kinda flipped the condition when writing that, i.e. "A != B is valid if A != B is valid", but it's actually "... if A != B is invalid". – Xeo Apr 09 '13 at 21:59
  • @Xeo Sure, intrusively we can write `struct auto_rel_ops {}; template::value || std::is_base_of::value) && can_equal::value>...> bool operator!=( U&& lhs, T&& rhs ) { return !(lhs == rhs); }` and etc for `>` and `<=` and `>=`, but I'm seeing if I can inject it both safely and non-intrusively... The goal is that compiling code *should not change* behavior with what I'm doing, and otherwise non-compiling code should do fallbacks only if they work... ie, a harmless expansion of legal operations. – Yakk - Adam Nevraumont Apr 09 '13 at 22:19
  • 1
    @Yakk: I can't think of a way for that goal to be accomplished. Think of an overload set that is restricted on `decltype(a != b)` being ill-formed - after introducing your `operator!=`, the call will suddenly be changed for types that weren't comparable with `!=` before. – Xeo Apr 09 '13 at 22:35
  • @Xeo naturally: the existence of "expression" SFINAE means that adding any missing operations or methods is a potentially breaking change (a hazard, but worth it). But I can definitely approximate it. – Yakk - Adam Nevraumont Apr 09 '13 at 22:39

1 Answers1

13

The problem with your approach seems to be that the fallback global definition of operator != is too attractive, and you need a SFINAE check to rule it out. However, the SFINAE check depends on the eligibility of the function itself for overload resolution, thus leading to an (attempted) infinite recursion during type deduction.

It seems to me that any similar attempt based on SFINAE would crash against the same wall, so the most sane approach is in my opinion to make your operator != a bit less appealing for overload resolution in the first place, and let other, reasonably written (this will be clear in a moment) overloads of operator != take precedence.

Given the type trait can_equal you provided:

#include <type_traits>
#include <functional>

template<typename T, typename U, typename=void>
struct can_equal : std::false_type {};

template<typename T, typename U>
struct can_equal<
   T,
   U,
   typename std::enable_if<
      std::is_convertible<
         decltype( std::declval<T>() == std::declval<U>() ),
         bool
      >::value
   >::type
>: std::true_type {};

I would define the fallback operator != this way:

template<typename T, typename U>
bool is_not_equal(T&& t, U&& u)
{
    return !(std::forward<T>(t) == std::forward<U>(u));
}

template<
    typename T,
    typename... Ts,
    typename std::enable_if<can_equal<T, Ts...>::value>::type* = nullptr
    >
bool operator != (T const& t, Ts const&... args)
{
    return is_not_equal(t, args...);
}

As far as I know, any overload of operator != that will define exactly two function parameters (so no argument pack) will be a better fit for overload resolution. Therefore, the above, fallback version of operator != will be picked only when no better overload exist. Moreover, it will be picked only if the can_equal<> type trait will return true.

I've tested this against the SSCCE you prepared, where four structs are defined together with some overloads of operator == and operator !=:

struct test { };

bool operator==(const test&, const test&) { std::cout << "(==)"; return true; }
bool operator!=(const test&, const test&) { std::cout << "(!==)"; return true; }

struct test2 { };

struct test3 { };
bool operator == (const test3&, const test3&) 
{ std::cout << "(==)"; return true; }

struct test4 { };

template<typename T, 
         EnableIf< std::is_convertible< T, test4 const& >::value >... >
bool operator == ( T&&, T&& ) { std::cout << "(==)"; return true; }

template<typename T, 
         EnableIf< std::is_convertible< T, test4 const& >::value >... >
bool operator != ( T&&, T&& ) { std::cout << "(!=)"; return true; }

To verify that the desired output is produced and mirror what you did in your original version of the fallback operator !=, I added a printout to is_not_equal():

template<typename T, typename U>
bool is_not_equal(T&& t, U&& u)
{
    std::cout << "!"; // <== FOR TESTING PURPOSES
    return !(std::forward<T>(t) == std::forward<U>(u));
}

Here are the three tests from your example:

std::cout << (a != b) << "\n"; // #1
std::cout << (test3() != test3()) << "\n"; // #2
std::cout << (test4() != test4()) << "\n"; // #3

Concerning the first test, operator != is defined for type test, so line #1 should print:

(!==)1

Regarding the second test, operator != is not defined for test3, and test3 is not convertible to test4, so our global operator != should come into play and negate the result of the overload of operator == that takes two const test3&. Therefore, line #2 should print:

!(==)0 // operator == returns true, and is_not_equal() negates it

Finally, the third test involves two rvalue objects of type test4, for which operator != is defined (because the arguments are convertible to test4 const&). Therefore, line #3 should print:

(!=)1

And here is a live example showing that the output produced is the expected one.

Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
  • @ipc: Thank you, but it still does not seem to give the same result as the OP's test case. Or maybe I'm misinterpreting it. As a side note, the SSCCE provided by the OP gives different results with Clang and GCC, so I'm wondering which compiler to trust – Andy Prowl Apr 09 '13 at 23:30
  • The strange thing is that this seems to be legal as of `13.5/1` because a a function parameter pack counts as exactly one function parameter. Not sure if this is intended by the standard. – ipc Apr 09 '13 at 23:41
  • @ipc: Actually I am starting to believe I am getting the right results (unless I am missing something). Here is the [test case](http://liveworkspace.org/code/3kW04t$35) – Andy Prowl Apr 09 '13 at 23:42
  • SFINAE technique I used with `EnableIf<>...` doesn't work on `clang` -- the `EnableIf<>...` always accepts. – Yakk - Adam Nevraumont Apr 10 '13 at 12:58
  • @Yakk: Yes, I noticed that. Btw does this solution look OK to you? – Andy Prowl Apr 10 '13 at 13:04
  • Well, there is the quibbling `struct silly { bool operator==( silly& o ) { return true; } };` which breaks it. Should I only be negating (and testing for the existence of) `const` `==` operators? – Yakk - Adam Nevraumont Apr 10 '13 at 14:30
  • @Yakk: Yes, that's actually a limitation which I tried to summarize under the "reasonably written" disclaimer at the beginning of my answer. But I will try and see if it can be patched – Andy Prowl Apr 10 '13 at 14:33