用于确定类型是否具有方法的 SFINAE

SFINAE to determine if a type has a method

本文关键字:有方法 SFINAE 是否 类型 用于      更新时间:2023-10-16
template <typename T>
struct has_xxx {
 private:
  using true_type = char;
  using false_type = long;
  template <typename C>
  static true_type has_xxx_impl(decltype(&C::xxx)); // comment 1
  template <typename C>
  static false_type has_xxx_impl(...); // comment 2
 public:
  enum { value = sizeof(has_xxx_impl<T>(0)) == sizeof(true_type) }; // comment3
};
struct Foo { int xxx() {return 0;}; };
struct Foo2 {};
int main() {
   static_assert(has_xxx<Foo>::value, "");
}

这是一个用于检测结构是否具有特定方法的结构。我对代码有一些疑问。

  1. 在评论 1 中,这个"&C"是什么意思,为什么我不能只写"C::xxx"
  2. 在注释 2 中,参数"..."是什么意思是,它是一个参数包,还是代表任何类型的参数
  3. 在评论 3 中,has_xxx_impl(0) 如何工作?T 被 Foo 替换,那么参数 0 怎么样?为什么选择第一个函数?

在评论 1 中,这个"&C"是什么意思,为什么我不能只写"C::xxx"

&实际上指的是&C::xxxxxx部分。也就是说,获取C内成员xxx的地址(希望这是一个函数,而不是静态成员---稍后会详细介绍)

在注释 2 中,参数"..."是什么意思是,它是一个参数包,还是代表任何类型的参数

... 或省略号运算符是一个接受任何内容的可变参数。这是一种非常 C 风格的做事方式,通常不是很安全。你今天最能看到的地方是吞咽例外:

try{
ThrowableFunctionCall();
} catch(...) // swallow any exceptions
{}

(注意:这不是参数包,也不是折叠表达式的一部分。请参阅 §5.2.2/6)

在评论 3 中,has_xxx_impl(0) 如何工作?T 被 Foo 替换,那么参数 0 怎么样?为什么选择第一个函数?

sizeof(has_xxx_impl<T>(0)) == sizeof(true_type)是一种使用重载分辨率将值设置为 true_typefalse_type 的聪明方法。请允许我解释一下:

乍一看调用has_xxx_impl<T>会让人觉得

template <typename C>
static true_type has_xxx_impl(decltype(&C::xxx)); // comment 1
template <typename C>
static false_type has_xxx_impl(...); // comment 2

是可行的候选者,因为它们具有相同的名称,并且可能都可以使用类型 T 进行实例化。然而

decltype(&C::xxx)

正在尝试创建指向成员类型的指针(例如 int(C::*)()成员函数指针),它是在类型推断的上下文中这样做的。如果语句格式不正确,则 SFINAE 生效并使其成为一个糟糕的候选者,只剩

下我们
template <typename C>
static false_type has_xxx_impl(...); // comment 2

由于...匹配任何内容,因此该值设置为 false_typelong .

如果语句格式正确,则可以将0正确分配给函数指针类型(它不情愿地转换为具有null值的指针类型)。这种转换优于匹配...(自己尝试!),因此该值采用true_type这是一个char

最后

value = sizeof(has_xxx_impl<T>(0)) == sizeof(true_type)

我们可以说是比较char的大小和long的大小(没有xxx的情况)或 longlong 大小的大小(具有xxx大小写)。

请注意,所有这些在 C++11/14 中都是完全不必要的,如果 xxx 是成员变量而不是函数,则确实会失败:

struct Foo3{static const int xxx;};

melak47的评论虽然更简单,但也有类似的缺点。

更好的解决方案是使用 is_member_function_pointer 类型特征:

template<typename...> // parameter pack here
using void_t = void;
template<typename T, typename = void>
struct has_xxx : std::false_type {};
template<typename T>
struct has_xxx<T, void_t<decltype(&T::xxx)>> :
  std::is_member_function_pointer<decltype(&T::xxx)>{};

现场演示

以下解释是不完美的,没有引用标准。我试图尽可能容易理解地解释它。

has_xxx_impl(decltype(&C::xxx))接受由decltype(&C::xxx)
产生的类型 &C::xxx是指向成员的指针,decltype(&C::xxx)是其类型。

另一个has_xxx_impl(...)接受"任何东西"

因此,当你有has_xxx_impl<T>编译器必须选择,上面之一。它更喜欢更好的匹配。因此,当类型包含xxx时,第一个版本比...版本匹配得更好。 如果它不包含这样的成员,则无法选择该函数,并且必须使用另一个(...

感谢SFINAE - 替换失败(尝试传递不存在的成员类型时)不会产生错误(选择其他重载)

请注意,这两个函数返回不同的类型(true_typefalse_type被定义为具有不同的大小),因此通过sizeof(has_xxx_impl<T>(0)) == sizeof(true_type)您可以判断选择了哪一个。此比较的结果设置为可从外部访问的value