How to avoid this sentence is false in a template

2019-02-01 18:55发布

问题:

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.

回答1:

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.