为什么 SFINAE 在这种情况下对我来说工作不正确以及如何解决它?

Why SFINAE works incorrect for me in this case and how to fix it?

我试图在 struct A 中留下一个函数 foo(打印 0),如果它的参数有模板方法 isA<void>,另一个(打印 1)如果没有。此代码(减少到下面的最小示例)编译(尝试使用 gcc 6.1.0 和 clang-3.9.0 以及显式 --std=c++14 选项)并运行。

但它会打印 1,不过我敢肯定,它会打印 0。我想知道我哪里错了,但真正的问题是:如何使这项工作正确?

请仅使用 C++14 解决方案。

#include <type_traits>
#include <iostream>
#include <utility>

using std::enable_if;
using std::declval;
using std::true_type;
using std::false_type;
using std::cout;

template<int M>
struct ObjectX
{
  template<typename C>
  bool isA() { return false; }
};

struct XX : ObjectX<23456> {
  int af;
};

template <typename ObjType> using has_dep = decltype(declval<ObjType>().template isA<void>());

template <typename, typename = void>
struct has_isa : public false_type {};

template <typename ObjType>
struct has_isa<ObjType, has_dep<ObjType> > : public true_type {};

template<typename ObjType>
struct A
{
  template<typename T = void>
  typename enable_if<has_isa<ObjType>::value, T>::type
  foo() {
    cout << "called foo #0" << "\n";
  }

  template<typename T = void>
  typename enable_if<!has_isa<ObjType>::value, T>::type
  foo() {
    cout << "called foo #1" << "\n";
  }
};

int
main()
{
  A<XX> axx;
  // XX().template isA<void>(); -- to check, that we can call it and it exists
  axx.foo();
  return 0;
}

你的 sfinae 失败了,因为 has_isa 选择了错误的专业。

has_isa<T> 的使用必须是默认实现或专用版本。

如您所定义,您有一个默认参数 void:

//   default argument ---------v
template <typename, typename = void>
struct has_isa : public false_type {};

那么在表达式has_isa<T>中,第二个参数必须为void。和has_isa<T, void>.

的写法大致相同

问题是这样的:

template <typename ObjType>
struct has_isa<ObjType, has_dep<ObjType>> : public true_type {};
//                      ^--- what's that type?

尽管模板部分排序会认为这个 "overload" 更专业,但它不会被选中。看has_dep的定义:

struct XX {
  template<typename C> bool isA() { return false; }
};

template <typename ObjType>
using has_dep = decltype(declval<ObjType>().template isA<void>());

嘿,has_dep<T> 类型是 t.isA<void>() 的 return 类型,即 bool!

所以专业版是这样的:

template <typename ObjType>
struct has_isa<ObjType, has_dep<ObjType>> : public true_type {};
//                      ^--- really, this is bool in our case

因此,为了使其正常工作,您必须调用 has_isa<T, bool>。由于这是不切实际的,您应该这样定义您的专业化:

template <typename ObjType>
struct has_isa<ObjType, void_t<has_dep<ObjType>>> : public true_type {};

其中 void_t 定义为:

template<typename...>
using void_t = void; // beware for msvc

因此,has_isa<T> 将始终考虑特化,因为我们将 void 作为第二个模板参数发送,现在我们的特化总是以 void 作为第二个参数。


此外,正如 Barry 所说,您的函数格式不正确,因为 sfinae 仅出现在直接上下文中。你应该这样写:

template<typename T = ObjType>
typename enable_if<has_isa<T>::value, void>::type
foo() { //                 ^--- sfinae happens with T
  cout << "called foo #0" << "\n";
}

如果您不想公开模板参数,只需将函数设为私有即可:

template<typename ObjType>
struct A {
public:
    void foo() {
        foo_impl();
    }

private:
    template<typename T = ObjType>
    typename enable_if<has_isa<T>::value, void>::type
    foo_impl() {
      cout << "called foo #0" << "\n";
    }

    template<typename T = ObjType>
    typename enable_if<!has_isa<T>::value, void>::type
    foo_impl() {
      cout << "called foo #1" << "\n";
    }
};

你的问题是你专攻错了class:

你应该强制 has_dep 到 return void

template <typename ObjType> using has_dep = decltype(static_cast<void>(declval<ObjType>().template isA<void>()));

所以这里

template <typename ObjType>
struct has_isa<ObjType, has_dep<ObjType> > : public true_type {};
// It is really <bjType, void> you specialize.

这个程序有两个问题。


首先,has_dep<XX>bool。当我们尝试 has_dep<XX> 时,添加默认模板参数意味着这实际上是 has_dep<XX, void>。但是专业化是 has_dep<XX, bool> - 这与我们实际查找的内容不匹配。 boolvoid 不匹配。这就是 has_dep<XX>false_type 的原因。这个问题的解决方案是 std::void_t,我建议通读 Q/A 以了解其工作原理。在你的专业中,你需要使用 void_t<has_dep<ObjType>> 来代替。


其次,这个不对:

template<typename T = void>
typename enable_if<has_isa<ObjType>::value, T>::type

SFINAE 只发生在替换的直接上下文中,class 模板参数不在函数模板替换的直接上下文中。这里的正确模式是:

template <typename T = ObjType> // default to class template parameter
enable_if_t<has_isa<T>>         // use the function template parameter to SFINAE
foo() { ... }

修复这两个问题,程序就会按预期运行。