Search code examples
c++templatesc++17overloadingsfinae

SFINAE does not properly disable ambiguous overloads


I'm trying to build a more flexible version of std::equal_to, which can be called on objects of two different types T1and T2 and only requires that T1::operator==(T2) or T2::operator==(T1) is defined. I'm using C++17. First, I have some struct that tests if SomeType::operator==(OtherType) is defined:

#include <vector>
#include <utility>
#include <functional>

template<class FromT, class ToT, class Dummy = void>
struct EqualityOperatorDefined : std::false_type {};

template<class FromT, class ToT>
struct EqualityOperatorDefined <FromT, ToT, decltype(std::declval<FromT>() == std::declval<ToT>())> 
  : std::true_type {};

I think that this works - at least, replacing the usage of EqualityOperatorDefined with true or false in the following does not make the problem go away. The actual flexible equal_to is this:

template<class T1, class T2>
class FlexibleEqualsTo
{
private:
    static constexpr bool cmpT1toT2 = EqualityOperatorDefined<T1, T2>::value;
    static constexpr bool cmpT2toT1 = EqualityOperatorDefined<T2, T1>::value;
public:
    template<class Dummy = T1>
    constexpr bool operator()(const std::enable_if_t<cmpT1toT2, Dummy> & lhs, const T2& rhs)
    {
        return lhs == rhs;
    }

    template<class Dummy = T1>
    constexpr bool operator()(const std::enable_if_t<cmpT2toT1 && !cmpT1toT2, Dummy> & lhs, const T2& rhs)
    {
        return rhs == lhs;
    }
};

int main() {
    FlexibleEqualsTo<int, int> foo;
}

You can find the complete code here at godbolt.

However, GCC and Clang both complain about erroneous overloads here. GCC tells me:

<source>: In instantiation of 'class FlexibleEqualsTo<int, int>':
<source>:32:32:   required from here
<source>:25:17: error: 'template<class Dummy> constexpr bool FlexibleEqualsTo<T1, T2>::operator()(std::enable_if_t<(FlexibleEqualsTo<T1, T2>::cmpT2toT1 && (! FlexibleEqualsTo<T1, T2>::cmpT1toT2)), Dummy>&, const T2&) [with Dummy = Dummy; T1 = int; T2 = int]' cannot be overloaded with 'template<class Dummy> constexpr bool FlexibleEqualsTo<T1, T2>::operator()(std::enable_if_t<FlexibleEqualsTo<T1, T2>::cmpT1toT2, Dummy>&, const T2&) [with Dummy = Dummy; T1 = int; T2 = int]'
   25 |  constexpr bool operator()(const std::enable_if_t<cmpT2toT1 && !cmpT1toT2, Dummy> & lhs, const T2& rhs)
      |                 ^~~~~~~~
<source>:19:17: note: previous declaration 'template<class Dummy> constexpr bool FlexibleEqualsTo<T1, T2>::operator()(std::enable_if_t<FlexibleEqualsTo<T1, T2>::cmpT1toT2, Dummy>&, const T2&) [with Dummy = Dummy; T1 = int; T2 = int]'
   19 |  constexpr bool operator()(const std::enable_if_t<cmpT1toT2, Dummy> & lhs, const T2& rhs)
      |                 ^~~~~~~~

Only one of the two boolean expressions in std::enable_if can be true. Thus, the other (in this case: the second) definition of operator() should be SFINAEed away, and there should be no overloading problems.

I think I avoided the usual pitfalls of applying SFINAE. Both function templates depend on their template parameter.

What is going wrong here?


Solution

  • Your SFINAE detector for operator== is bugged. Replace this:

    decltype(std::declval<FromT>() == std::declval<ToT>())
    

    with this:

    decltype(std::declval<FromT>() == std::declval<ToT>(), void())