这个SFINAE C++语法是如何工作的?

3

我刚开始涉猎SFINAE,但是在最常用的例子的语法上遇到了麻烦。这个例子有很多不同形式,但是它的主要思想是检查一个特定类型是否包含给定成员。这个例子来自维基百科

template <typename T> struct has_typedef_foobar 
{
    typedef char yes[1];
    typedef char no[2];

    template <typename C> static yes& test(typename C::foobar*);
    template <typename> static no& test(...);

    static const bool value = sizeof(test<T>(0)) == sizeof(yes);
};

对于这个问题,我有几点不理解:

  1. 返回"yes"的test()重载函数的参数类型是什么?它是一个指针吗?为什么使用typename关键字作为参数的一部分?我也看到过它用于测试一个类是否具有给定类型的成员,而不仅仅是typedef,语法保持不变。
  2. 有时我看到的示例使用test(int C ::*)。这更奇怪,不知道我们正在引用C的哪个成员。如果它是一个具有实体和实际类型的真实函数,并且参数已命名,则它将指向什么,并且如何使用它?
    template <typename T> func(int T::*arg)
    {
        *arg = 1;
    }
    struct Foo { int x; } foo;
    func<Foo>(&foo::x); // 类似于这样? func(&foo::x); // 或者甚至像这样?
  3. 如果在第二个重载中未使用,则是否允许使用没有符号的template <typename>?那么它怎么成为一个模板函数呢?
  4. 额外加分:它能检查多个成员的存在吗?
3个回答

5

这些问题大多数与SFINAE无关:

  1. 当一个依赖名称应被视为类型时,需要在它前面加上typename。由于Ctest()的模板参数,因此C::foobar显然是一个依赖名称。尽管函数声明要求在每个参数前面有一个类型,但语言要求使用typename将依赖名称C::foobar转换为类型。因此,typename C::foobar只是一个类型,并且对它应用类型构造函数*会产生相应的指针类型。
  2. int C::*是一个未命名的指向类型为int的数据成员的指针。
  3. 未使用的名称总是可以省略的。这适用于函数参数以及模板参数,也就是说,在template之后的名称如果没有被使用可以省略。不过,大多数情况下名称会以某种形式被使用,那么就需要明确指出。
  4. 我认为你可以编写一个测试来测试多个方面的存在,但我不愿意这样做:SFINAE已经足够难以理解了。我宁愿使用普通的逻辑运算符明确地组合不同的属性测试。

好的,我仍然不清楚第2点,这应该指向C的哪个数据成员的指针?如果这是一个真正的函数并且参数被命名了,它将指向什么? - neuviemeporte
@neuviemeporte:它不会指向任何成员——它传递了0,因此它是一个空指针。唯一重要的是,成员指针声明是否有效。 - Xeo
是的,但我的意思是在这个SFINAE场景之外,这个语法是什么意思?如果我使用某种类型C实例化该函数并传递指向现有实例的指针,那么参数int C :: * arg是什么意思?它是否合法? - neuviemeporte
@neuviemeporte:假设你的类C有两个int成员,比如xy,并且有一个函数可以处理其中任意一个。你可以将&C::x&C::y传递给该函数以指定使用哪一个。这些的类型将是int (C::*arg)(我认为在给成员指针命名时需要括号),你可以使用this->*arg来访问相应的成员。这些家伙被称为_成员指针_。 - Dietmar Kühl
@DietmarKühl,你能否请看一下我添加到2)的代码示例并告诉我是否正确吗? 我很困惑的是作用域运算符被用于C, 但是没有一个名为C::的成员。 - neuviemeporte
3
@neuviemeporte: int C::* 是一个指向成员的指针:你可以像其他指针一样随意命名。在这方面,它就像任何其他指针一样。然而,成员指针并不是真正的指针,因为它们不指向内存中的地址。相反,它们更像是一个偏移量,指定如何访问给定类的特定成员,给定该类的对象。您可以使用 template <typename T> void f(T& obj, int T::*mem) { obj.*mem = 17; } 与您的类(假设成员是 public)一起使用,例如 Foo foo; f(foo, &Foo::x); - Dietmar Kühl

3
这个SFINAE的例子依赖于这样一种事实:当进行重载决议时,其参数列表为... 的函数是最不受欢迎的。
因此,首先,编译器将尝试。
static yes& test(typename C::foobar*);

通过将 C 替换为实际类型。如果 C 有一个名为 foobar 的成员类型,它将被编译并选择。如果没有,则会编译失败,并选择 ... 过载。它总是可以编译的。因此,回答您的第一个问题,返回 yes& 的类型是具有成员类型 foobar 的任何类型。

typename 关键字用于 依赖类型:即依赖于模板参数的类型。因为这些类型既可以是变量名也可以是类型名,所以编译器默认它是变量名,除非您使用 typename 明确告诉它。理论上来说,编译器是可以自行检查的,但这会使编译器更加复杂,因此没有这样做。

至于您的第二个问题,那是一个成员变量指针。您还可以拥有成员函数指针,其完整形式实际上类似于 void test(int(C::*arg_name)())

对于第三个问题,是允许的,但模板参数从未被使用过。由于您没有使用推导,而是显式指定参数,因此没问题。就像无名称的普通参数一样,例如 void f(int);

至于第四个问题,是可以的,但是按我所知的方法,您只需要为要测试的 n 个成员编写 n*2 个函数即可。对于两个成员,它看起来像这样:

template <typename C> static yes& test1(typename C::foobar*);
template <typename> static no& test1(...);

template <typename C> static yes& test2(typename C::quux*);
template <typename> static no& test2(...);

static const bool value = sizeof(test1<T>(0)) + sizeof(test2<T>(0)) == sizeof(yes) * 2;

谢谢,对于依赖类型中变量名和类型名之间区别的澄清打了+1。 - neuviemeporte

0

1) 在模板中,如果参数是模板参数的依赖类型,则需要使用typename。在这种情况下,C::foobar是参数C的依赖类型。test()的参数类型是指向C::foobar的指针。如果C::foobar不是类型,则该版本的test重载将无法编译,并找到另一个重载版本。

uk4321涵盖了其余部分。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接