Ответ 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, показывающий, что полученный результат является ожидаемым.