Home > front end >  SFINAE detection: substitution succeeds when compilation fails
SFINAE detection: substitution succeeds when compilation fails

Time:02-03

I wrote a simple SFINAE-based trait to detect whether a class has a method with a specific signature (the back story is that I am trying to detect whether a container T has a method T::find(const Arg&) - and if yes, then store a pointer to this method).

After some testing I discovered a case where this trait would give a false positive result - if T defines the method as a template but definition compiles not for all possible Ts. This is best demonstrated with an example:

#include <iostream>
#include <string>
#include <vector>
#include <type_traits>

using namespace std;

// HasFooWithArgument is a type trait that returns whether T::foo(const Arg&) exists.

template <typename T, typename Arg, typename = void>
inline constexpr bool HasFooWithArgument = false;

template <typename T, typename Arg>
inline constexpr bool HasFooWithArgument<T, Arg, std::void_t<decltype(std::declval<const T&>().foo(std::declval<const Arg&>()))>> = true;

// Below are the test cases. struct E is of the most interest.

struct A {};
struct B {
  void foo(int x) const {}
};
struct C {
  void foo(int x) const {}
  void foo(const std::string& x) const {}
};

struct D {
  template <typename T>
  void foo(const T& x) const {}
};
struct E {
  // Any T can be substituted (hence SFINAE "detects" the method)
  // However, the method compiles only with T=int!
  // So effectively, there is only E::foo(const int&) and no other foo methods...
  template <typename T>
  void foo(const T& x) const {
    static_assert(std::is_same_v<T, int>, "Only ints are supported");
  }
};

int main()
{
    cout << std::boolalpha;
    cout<<"A::foo(int): " << HasFooWithArgument<A, int> << "\n";
    cout<<"B::foo(int): " << HasFooWithArgument<B, int> << "\n";
    cout<<"C::foo(int): " << HasFooWithArgument<C, int> << "\n";
    cout<<"C::foo(string): " << HasFooWithArgument<C, std::string> << "\n";
    cout<<"C::foo(std::vector<int>): " << HasFooWithArgument<C, std::vector<int>> << "\n";
    cout<<"D::foo(string): " << HasFooWithArgument<D, std::string> << "\n";
    cout<<"E::foo(int): " << HasFooWithArgument<E, int> << "\n";
    cout<<"E::foo(string): " << HasFooWithArgument<E, std::string> << "\n";
    
    E e;
    e.foo(1); // compiles
    // e.foo(std::string()); // does not compile

    return 0;
}

The above prints:

A::foo(int): false
B::foo(int): true
C::foo(int): true
C::foo(string): true
C::foo(std::vector<int>): false
D::foo(string): true
E::foo(int): true
E::foo(string): true

Unfortunately, this last case is my main target: as I mentioned above, I am doing this to detect container find methods and they are usually defined as templates to support heterogeneous lookup.

In other words, std::set<std::string, std::less<>> is where my detection gives false positives as it returns true for any argument type.

Thanks in advance!

CodePudding user response:

The issue you're describing is not due to the fact that SFINAE doesn't work properly with function templates (it does). It has to do with the fact that SFINAE can't detect whether a function body is well-formed.

Your approach should work just fine when it comes to detecting that std::set<std::string> does not have a find member that takes a std::string_view argument. This is because the templated find member does not participate in overload resolution unless the set has a transparent comparator. Since std::set<std::string> doesn't have a transparent comparator, its find function will only accept types that are implicitly convertible to std::string; any attempt to pass any other type will result in an overload resolution failure, which will be detected by SFINAE just like any other case of an argument type mismatch.

Where your approach won't work is with something like std::set<std::string, std::less<>> and trying to call find with an argument type of say, int. In this case the detection will succeed because the find template is unconstrained, but then compilation will fail later on if you try to actually call find with an int argument, because somewhere in the body, it will try to call std::less<> with types that can't be compared with each other (std::string and int).

Colloquially, we say that a function or a class that makes it possible to detect using SFINAE whether it, itself, would be well-formed, is "SFINAE-friendly". In that sense, std::set::find fails to be SFINAE-friendly. To make it SFINAE-friendly, it would have to guarantee that an overload resolution failure would occur if you tried to call it with a type that isn't comparable using the specified transparent comparator.

When something is not SFINAE-friendly, the only workaround is to special-case it: in other words, you have to put in your code a special case that makes detection fail for std::set::find in this case, by checking explicitly whether the comparator accepts the argument type. (std::less<> is SFINAE-friendly: it does the overload resolution in the (explicitly specified) return type.)

  •  Tags:  
  • Related