10

I was trying to detect the presence of a member function baz() in a template parameter:

template<typename T, typename = void>
struct ImplementsBaz : public std::false_type { };

template<typename T>
struct ImplementsBaz<T, decltype(&T::baz)> : public std::true_type { };

But it always produces false:

struct Foo {};
struct Bar { void baz() {} };

std::cout << ImplementsBaz<Foo>::value << std::endl;  // 0
std::cout << ImplementsBaz<Bar>::value << std::endl;  // also 0

Using declval and calling the method does work, though:

template<typename T>
struct ImplementsBaz<T, decltype(std::declval<T>().baz())> : public std::true_type { };

Of course, now this can only detect a baz function with 0 arguments. Why is the specialization correctly selected when using declval<T>().baz(), but not decltype(&T::baz)?

max66
  • 65,235
  • 10
  • 71
  • 111
jtbandes
  • 115,675
  • 35
  • 233
  • 266

4 Answers4

6

If you use the void_t "detection idiom", then it does work as expected:

template <typename...> using void_t = void;

template <typename T>
struct ImplementsBaz<T, void_t<decltype(&T::baz)>> : std::true_type {};


struct Bar { void baz() {} };

static_assert(ImplementsBaz<Bar>::value); // passes

Godbolt link

As to why, this question explains in detail how the "void_t trick" works. To quote from the accepted answer:

It's as if you had written has_member<A, void>::value. Now, the template parameter list is compared against any specializations of the template has_member. Only if no specialization matches, the definition of the primary template is used as a fall-back.

In the original case, decltype(&T::baz) is not void, so the specialization does not match the original template and so is not considered. We need to use void_t (or some other mechanism, such as a cast) to change the type to void so that the specialisation will be used.

Tristan Brindle
  • 16,281
  • 4
  • 39
  • 82
3

Try with

decltype(&T::baz, void())

Your example with decltype(std::declval<T>().baz()) and

struct Bar { void baz() {} };

works because baz() return void so the void match the default typename = void in the not specialized Implements_baz struct.

But if you define Bar as follows

struct Bar { int baz() { return 0; } };

you obtain false from Implement_baz because baz() return int that doesn't match void.

Same problem with decltype(&T::baz): doesn't match void because return the type of a method.

So the solution (well... a possible solution) is use decltype(&T::baz, void()) because return void if T::baz exist (or fail, and return nothing, if T::baz doesn't exist).

max66
  • 65,235
  • 10
  • 71
  • 111
  • This is probably the best, unless the user only wants to match functions T::baz with void return type. – jwimberley Jul 25 '17 at 18:15
  • 1
    @jwimberley - would be a different problem; in that case the OP solution is a good solution but remain the problem that works only if `baz()` receive no arguments (or if the developer know the types of the arguments). – max66 Jul 25 '17 at 18:20
1

This is because decltype(&T::baz) is an error and the partial specialization is never instantiated. There is no static member called baz in T (i.e. Bar).

The second one does the right thing, i.e. call the method on an instance and then use the return type of that.


If you want to detect the presence of the method regardless of what parameters you pass to it if there is only one overload.

template <typename Type, typename = std::enable_if_t<true>>
struct ImplementsBaz : public std::integral_constant<bool, true> {};
template <typename Type>
struct ImplementsBaz<Type, std::enable_if_t<
                         std::is_same<decltype(&T::baz), decltype(&T::baz)>
                             ::value>> 
    : public std::integral_constant<bool, false> {};

If you want to detect the presence of that method if it contains overloads, take a look at the member detection idiom. Basically it assumes that a method with that name exists and then if there is another method with that name then the traits class goes into error and selects the right true_type specialization or similar. Take a look!

Curious
  • 20,870
  • 8
  • 61
  • 146
  • I think your first answer was actually right — it seems the return type does matter. `&Bar::baz` should be a valid expression, so is there any way to do this using `decltype(&T::baz)`? – jtbandes Jul 25 '17 at 17:58
  • The discrepency persists even if baz is made static. – jwimberley Jul 25 '17 at 17:58
  • 1
    @jwimberley in that case the expression evaluates to an address, which is still not of type `void` – Curious Jul 25 '17 at 17:59
  • @jtbandes not sure if there is any other way but it's straightforward to use `declval` to pretend calling the method on an instance. That's what `declval` was meant for – Curious Jul 25 '17 at 18:00
  • @Curious How about making it argument-type agnostic? using `declval().baz()` requires explicitly passing arguments to baz, but I was interested in detecting the presence of a baz method regardless of argument type(s). – jtbandes Jul 25 '17 at 18:00
  • Why can’t it be a pointer-to-member? (Not that it has type `void`!) – Davis Herring Jan 31 '22 at 21:14
0

Another possible solution is to use

template<typename T>
struct ImplementsBaz<T, typename std::result_of<decltype(&T::baz)(T)>::type > : public std::true_type { };

Or, if you prefer for readability,

template<typename T>
using BazResult = typename std::result_of<decltype(&T::baz)(T)>::type;

template<typename T>
struct ImplementsBaz<T, BazResult<T> > : public std::true_type { };

This will only work if it just your intention to match functions T::baz with a void return type, although the same is true of your alternate working solution. It also has the deficiency of only working if there are no parameters, so it is only different from your second solution in style, unfortunately.

jwimberley
  • 1,696
  • 11
  • 24