Подтвердить что ты не робот

Как избежать этого предложения является ложным в шаблоне SFINAE?

Итак, я хочу написать автоматический !=:

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

но это невежливо 1. Поэтому я пишу

// 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 {};

который представляет собой класс признаков типов, который говорит: "is t == u допустимый код, который возвращает тип, конвертируемый в bool".

Итак, я улучшаю свой !=:

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) );
}

и теперь это только допустимое переопределение, если == существует. К сожалению, это немного жадный:

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

так как он будет хватать почти все test() != test(), а не вызывается выше !=. Я думаю, что это нежелательно - я бы скорее назвал явный !=, чем auto-forward, на == и отрицал.

Итак, я пишу этот класс признаков:

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

который проверяет, действительно ли T != U.

Затем мы увеличиваем != следующим образом:

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) );
}

который, если вы его разобрали, говорит "это предложение ложно" - operator!= существует между T и U iff operator!= не существует между T и U.

Неудивительно, что каждый компилятор я тестировал segfaults, когда его кормили. (clang 3.2, gcc 4.8 4.7.2 intel 13.0.1). Я подозреваю, что то, что я делаю, является незаконным, но мне бы хотелось увидеть стандартную ссылку. (edit: то, что я делаю, является незаконным, поскольку оно вызывает неограниченное рекурсивное расширение шаблона, если мой != применяется, мы должны проверить, применим ли мой !=. Версия, связанная в комментариях с #if 1, дает разумную ошибку).

Но мой вопрос: есть ли способ убедить моего основанного на SFINAE переопределения игнорировать "себя" при принятии решения о том, должен ли он потерпеть неудачу или нет или каким-то образом избавиться от самореференциальной проблемы? Или понизить приоритет моего operator!= до минимума, чтобы выиграть какой-либо явный !=, даже если это не так хорошо соответствует?

Тот, который не проверяет, что "!= не существует" работает достаточно хорошо, но недостаточно для меня, чтобы быть таким же невежливым, как внедрить его в глобальное пространство имен.

Цель - любой код, который будет компилироваться без моего "волшебного" !=, делает то же самое, как только будет введено мое "волшебное" !=. Если и только если != в противном случае недействителен и bool r = !(a==b) хорошо сформирован, если мой "волшебный" != ударит.


Сноска 1: если вы создаете template<typename U, typename T> bool operator!=(U&& u, T&& t), SFINAE будет думать, что каждая пара типов имеет действительный != между ними. Затем, когда вы пытаетесь на самом деле вызвать !=, он создается и не компилируется. Кроме того, вы топаете в функциях bool operator!=( const foo&, const foo& ), потому что вы лучше сочетаетесь для foo() != foo() и foo a, b; a != b;. Я считаю, что оба эти невежливые.

4b9b3361

Ответ 1

Проблема с вашим подходом заключается в том, что глобальное определение operator != является слишком привлекательным, и для его устранения требуется проверка SFINAE. Однако проверка SFINAE зависит от возможности самой функции для разрешения перегрузки, что приводит к (попытке) бесконечной рекурсии при выводе типа.

Мне кажется, что любая подобная попытка, основанная на SFINAE, потерпела бы крах против одной и той же стены, поэтому самый разумный подход, на мой взгляд, должен сделать ваш operator != менее привлекательным для разрешения перегрузки в первую очередь, и пусть другие, разумно написанные (это будет ясно в какой-то момент) перегрузки operator != имеют преимущество.

Учитывая характерный признак can_equal, вы указали:

#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 {};

Я бы определил резервный operator != следующим образом:

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...);
}

Насколько мне известно, любая перегрузка operator !=, которая будет определять ровно два параметра функции (так что пакет аргументов) не будет лучше подходит для разрешения перегрузки. Следовательно, вышеприведенная резервная версия operator != будет выбрана только тогда, когда не будет лучшей перегрузки. Более того, он будет выбран, только если символ типа can_equal<> вернет true.

Я тестировал это против подготовленного вами SSCCE, где четыре struct определены вместе с некоторыми перегрузками operator == и 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; }

Чтобы убедиться, что вы хотите получить нужный результат и зеркалировать то, что вы сделали в исходной версии резервной operator !=, я добавил распечатку на 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));
}

Вот три теста из вашего примера:

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

Что касается первого теста, operator != определяется для типа test, поэтому строка #1 должна печатать:

(!==)1

Что касается второго теста, operator != не определен для test3, а test3 не конвертируется в test4, поэтому наш глобальный operator != должен вступить в игру и отрицать результат перегрузки operator ==, который принимает два const test3&. Поэтому строка #2 должна печатать:

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

Наконец, третий тест включает в себя два объекта rvalue типа test4, для которых operator != определен (поскольку аргументы конвертируются в test4 const&). Поэтому строка #3 должна печатать:

(!=)1

И вот пример live, показывающий, что полученный результат является ожидаемым.