AlphaRL
AlphaRL

Reputation: 1629

SFINAE to determine if a type has a method

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

This is a struct to detect whether a struct has a certain method. I have some questions about the code.

  1. In comment 1, what does this '&C' mean, why can't I just write 'C::xxx'
  2. In comment 2, what does the parameter '...' mean, is it a parameter pack, or it represents any kind of parameter
  3. In comment 3, how does has_xxx_impl(0) work? T is replaced by Foo, then how about the parameter 0? why the first function is selected?

Upvotes: 2

Views: 468

Answers (2)

AndyG
AndyG

Reputation: 41220

In comment 1, what does this '&C' mean, why can't I just write 'C::xxx'

The & is really referring to the xxx portion of &C::xxx. That is, get the address of the member xxx within C (which is hopefully a function, and not a static member --- more on that later)

In comment 2, what does the parameter '...' mean, is it a parameter pack, or it represents any kind of parameter

the ..., or ellipsis operator is a variadic argument that accepts anything. This is a pretty C-style way of doing things, and usually isn't very typesafe. The place you'll see it most today is in swallowing exceptions:

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

(Note: this is NOT a parameter pack, or part of a fold expression. Refer to §5.2.2/6)

In comment 3, how does has_xxx_impl(0) work? T is replaced by Foo, then how about the parameter 0? why the first function is selected?

sizeof(has_xxx_impl<T>(0)) == sizeof(true_type) is a sort-of clever way of using overload resolution to set the value to true_type or false_type. Allow me to explain:

calling has_xxx_impl<T> at first makes it appear that both of

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

Are viable candidates, because they have the same name, and could maybe both be instantiated with type T. However

decltype(&C::xxx)

Is attempting to create a pointer to member type (e.g. int(C::*)() for member function pointer) and it's doing so in a type-deduced context. If the statement is ill-formed, then SFINAE takes effect and makes it a bad candidate, leaving us with only

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

and since ... matches anything, the value is set to false_type or long.

If the statement is well-formed, then 0 can be properly assigned to a function pointer type (it is begrudgingly converted to a pointer type with a null value). This conversion is preferred over matching ... (try it yourself!), and hence the value takes on true_type which is a char.

Finally,

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

we can say is either comparing size of char to size of long (doesn't have a xxx case) or a size of long to a size of long (has a xxx case).

Note that all of this is completely unnecessary in C++11/14, and indeed fails if xxx is a member variable and not a function:

struct Foo3{static const int xxx;};

The comment by melak47, while simpler, has a similar downfall.

A better solution would be to use the is_member_function_pointer type trait:

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

Live Demo

Upvotes: 1

relaxxx
relaxxx

Reputation: 7834

Following explanation is imperfect and doesn't quote standard. I tried to explain it as understandable as possible.

has_xxx_impl(decltype(&C::xxx)) accepts type which is produce by decltype(&C::xxx)
&C::xxx is a pointer to member and decltype(&C::xxx) is its type.

the other one has_xxx_impl(...) accepts "anything"

so, when you have has_xxx_impl<T> compiler must choose, one of above. It prefers the better match. So when type contains xxx the first one is better match than the ... version. If it doesn't contain such member, the function cannot be chosen and it has to use the other one (...)

Thanks to SFINAE - the substitution failure (when trying to pass type of non-existent member) doesn't produce an error (other overload is chosen)

Notice, that both functions returns different types (true_type and false_type which are defined to have distinct sizes) so with sizeof(has_xxx_impl<T>(0)) == sizeof(true_type) you can tell, which one was chosen. The result of this comparison is set as a value which you can access from the outside.

Upvotes: 1

Related Questions