How does this SFINAE C++ syntax work?

2019-02-25 17:34发布

问题:

I have just begun dabbling with SFINAE and I am having trouble understanding the syntax behind the most often-used example which appears in various forms, but the idea is to check whether a particular type contains a given member. This particular one is from Wikipedia:

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

There are a couple of things I don't get about this:

  1. What is the argument type of the test() overload that returns "yes"? Is it a pointer? Why the typename keyword used as part of the argument? I've seen it used also for testing if a class has a member of a given type, not just a typedef, and the syntax remains unchanged.
  2. Sometimes I've seen examples that use test(int C::*). This is even more strange, no idea what member of C are we referencing. If it was a real function with a body, instantiated for a real type, and the argument was named, what would it point to and how would you use it?
    template <typename T> func(int T::*arg)
    {
        *arg = 1;
    }
    
    struct Foo
    {
        int x;
    } foo;
    
    func<Foo>(&foo::x); // something like this?
    func(&foo::x); // or maybe even like this?
    
  3. Is template <typename> with no symbol allowed if it's not being used in the second overload? How is it even a template function then?
  4. Bonus: Could it be made to check for existence of more than one member at a time?

回答1:

Most of these questions have nothing to do with SFINAE:

  1. When a dependent name should be considered a type, it needs to be preceded by typename. Since C is a template parameter to test(), clearly C::foobar is a dependent name. Even though function declarations require a type in front of each argument, the language requires the use of typename to turn the dependent name C::foobar into a type. With that, typename C::foobar is just a type and applying a the type constructor * to it, produces the corresponding pointer type.
  2. int C::* is an unnamed pointer to data member of type int.
  3. Names which are not used can always be left out. This applies to function arguments as well as to the template parameters, i.e., yes, the name after template can be omitted if it isn't used. Most of the time it is used in some form, though, in which case it is, obviously, required.
  4. I'd think you can write a test which tests for the presence of multiple aspects but I wouldn't to it: SFINAE is unreadable enough as it is. I'd rather explicit combine the different property tests with normal logical operators.


回答2:

This example of SFINAE relies on the fact that a function whose parameter list is ... is the least preferred when doing overload resolution.

So first, the compiler will try the

static yes& test(typename C::foobar*);

By substituting C for the real type. If C has a member type named foobar, it will compile and be chosen. If not, it will fail to compile, and the ... overload will be chosen. It will always compile. So to answer your first question, the type that returns yes& is whatever has a member type foobar.

The word typename is required for dependent types: types that depend on a template parameter. Because these types could be either variable names or type names, the compiler assumes it's a variable name unless you tell it otherwise with typename. It theoretically could check for itself, but that would make the compiler even more complicated to write, which is apparently undesirable enough not to do it.

As for your second question, that's a member variable pointer. You can also have member function pointers whose full form is actually like void test(int(C::*arg_name)()).

As for three, yes, it is allowed but the template argument is never used. Since you're not using deduction, but explicitly specifying the parameter, it's fine. Just like an unnamed normal argument like void f(int);.

As for four, yes it can, but for the way I know of, you just have to have n * 2 functions for n members that you want to test for. For two, it would look like

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;


回答3:

1) typename is needed in templates where the parameter is a dependent type of one of the template parameters, in this case C::foobar is a dependent type of the parameter C. The argument type of test() is a pointer to a C::foobar. If C::foobar is anything but a type, that version of the test overload will fail to compile and another version of the overload will be found.

uk4321 covered the rest.