SFINAE检测:编译失败时替换成功

SFINAE detection: substitution succeeds when compilation fails

我写了一个简单的基于 SFINAE 的特征来检测 class 是否有一个带有特定签名的方法(背后的故事是我试图检测容器 T 是否有一个方法 T::find(const Arg&) - 如果是,则存储指向此方法的指针)。

经过一些测试后,我发现了这种特性会给出误报结果的情况 - 如果 T 将方法定义为模板,但定义并未针对所有可能的 T 进行编译。最好用一个例子来证明这一点:

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

以上打印:

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

不幸的是,最后一个案例是我的主要目标:正如我上面提到的,我这样做是为了检测容器查找方法,它们通常被定义为模板以支持异构查找。

换句话说,std::set<std::string, std::less<>> 是我的检测给出误报的地方,因为它 returns 对于任何参数类型都是正确的。

提前致谢!

您所描述的问题并不是因为 SFINAE 不能与函数模板一起正常工作(它确实如此)。这与SFINAE无法检测函数体是否为well-formed.

有关

在检测 std::set<std::string> 没有采用 std::string_view 参数的 find 成员时,您的方法应该可以正常工作。这是因为模板化的 find 成员 does not participate in overload resolution 除非集合有一个透明的比较器。由于 std::set<std::string> 没有透明的比较器,它的 find 函数将只接受可以隐式转换为 std::string 的类型;任何传递任何其他类型的尝试都将导致重载解析失败,这将被 SFINAE 检测到,就像任何其他参数类型不匹配的情况一样。

你的方法不起作用的地方是 std::set<std::string, std::less<>> 并尝试使用参数类型 int 调用 find。在这种情况下,检测将 成功 因为 find 模板不受约束,但是如果您尝试使用 int 参数,因为在正文的某处,它会尝试调用 std::less<> 具有无法相互比较的类型(std::stringint)。

通俗地说,我们说一个函数或一个 class 可以使用 SFINAE 检测它本身是否是 well-formed,是“SFINAE-friendly”。从这个意义上说,std::set::find 不是 SFINAE-friendly。要使其成为 SFINAE-friendly,它必须保证如果您尝试使用无法使用指定透明比较器进行比较的类型来调用它,则会发生重载解析失败。

当某些东西不是 SFINAE-friendly 时,唯一的解决方法是 special-case 它:换句话说,你必须在 你的 代码中加入一个特殊的在这种情况下,通过明确检查比较器是否接受参数类型,使 std::set::find 检测失败的情况。 (std::less<> SFINAE-friendly:它在(明确指定的)return 类型中执行重载解析。)