jtbandes
jtbandes

Reputation: 118671

Why does decltype(declval<T>().func()) work where decltype(&T::func) doesn't?

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)?

Upvotes: 11

Views: 4123

Answers (4)

max66
max66

Reputation: 66200

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).

Upvotes: 3

Tristan Brindle
Tristan Brindle

Reputation: 16824

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.

Upvotes: 6

Curious
Curious

Reputation: 21510

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!

Upvotes: 1

jwimberley
jwimberley

Reputation: 1748

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.

Upvotes: 0

Related Questions