14

So I'm very familiar with the paradigm of testing if a member function exists. Currently this code works:

#include <iostream>
#include <type_traits>

struct has_mem_func_foo_impl {
    template <typename U, U>
    struct chk { };

    template <typename Class, typename Arg>
    static std::true_type has_foo(chk<void(Class::*)(Arg), &Class::foo>*);

    template <typename, typename>
    static std::false_type has_foo(...);
};

template <typename Class, typename Arg>
struct has_mem_func_foo : decltype(has_mem_func_foo_impl::template has_foo<Class,Arg>(nullptr)) { };


struct bar {
    void foo(int) { }
};

int main() {
    static_assert( has_mem_func_foo<bar, int>::value, "bar has foo(int)" );
}

unfortunately if I make a slight adjustment:

#include <iostream>
#include <type_traits>

struct has_mem_func_foo_impl {
    template <typename U, U>
    struct chk { };

    template <typename Class, typename... Arg>
    static std::true_type has_foo(chk<void(Class::*)(Arg...), &Class::foo>*);

    template <typename, typename...>
    static std::false_type has_foo(...);
};

template <typename Class, typename... Arg>
struct has_mem_func_foo : decltype(has_mem_func_foo_impl::template has_foo<Class,Arg...>(nullptr)) { };


struct bar {
    void foo(int) { }
};

int main() {
    static_assert( has_mem_func_foo<bar, int>::value, "bar has foo(int)" );
}

my static assertion fails. I was under the impression that variadic template parameter packs are treated just the same when expanded into their places. Both gcc and clang produce a failed static assertion.

The real root of my question is thus, is this standard behavior? It also fails when testing for the presence of a variadic templated member function.

cdacamara
  • 223
  • 1
  • 7
  • So the problem I see is that the compiler cannot guarantee that more `...Arg` shouldn't be deduced. You pass in the first `Arg`, but it needs to also check against every additional `Arg`. Hmm. I think I have an idea if that is right. – Yakk - Adam Nevraumont Oct 14 '14 at 14:45
  • The solution is to move `typename... Arg` to `has_mem_func_foo_impl` template parameters, [like here](http://coliru.stacked-crooked.com/a/015c3128ced5882d) – Piotr Skotnicki Oct 14 '14 at 14:47
  • I can definitely see how that could happen, but we pass the parameter pack explicitly `int` in this case. – cdacamara Oct 14 '14 at 14:48
  • @cdacamara no, you pass the first element of the parameter pack: You do not tell it that the parameter pack is over. So it cannot assume it is. The compiler is free to deduce more, so it tries to deduce, cannot deduce from `nullptr_t`, and gives up on that overload. – Yakk - Adam Nevraumont Oct 14 '14 at 14:51

1 Answers1

8

The problem I see is that Arg... being passed int is not enough. It would be valid for the compiler to add new args to the end of it.

Deducing what to add to the end of it from nullptr_t isn't possible, so the compiler says "I give up, not this case".

But we don't need to have Arg... in a deducable context for your trick to work:

#include <iostream>
#include <type_traits>

template<class Sig>
struct has_mem_func_foo_impl;

template<class R, class...Args>
struct has_mem_func_foo_impl<R(Args...)> {
  template <typename U, U>
  struct chk { };

  template <typename Class>
  static constexpr std::true_type has_foo(chk<R(Class::*)(Args...), &Class::foo>*) { return {}; }

  template <typename>
  static constexpr std::false_type has_foo(...) { return {}; }
};

template <typename Class, typename Sig>
struct has_mem_func_foo :
  decltype(has_mem_func_foo_impl<Sig>::template has_foo<Class>(nullptr))
{};

struct bar {
  void foo(int) { }
};


int main() {
  static_assert( has_mem_func_foo<bar, void(int)>::value, "bar has foo(int)" );
}

we move the Args... to the class itself, then only pass in the type to the function. This blocks deduction, which makes nullptr conversion to the member function pointer doable, and things work again.

I also included some improved signature based syntax, which also means it supports return type matching.

Note that you may be asking the wrong question. You are asking if there is a member function with a particular signature: often what you want to know is if there is a member function that is invokable with a certain set of arguments, with a return type compatible with your return value.

namespace details {
  template<class T, class Sig, class=void>
  struct has_foo:std::false_type{};

  template<class T, class R, class... Args>
  struct has_foo<T, R(Args...),
    typename std::enable_if<
      std::is_convertible<
        decltype(std::declval<T>().foo(std::declval<Args>()...)),
        R
      >::value
      || std::is_same<R, void>::value // all return types are compatible with void
      // and, due to SFINAE, we can invoke T.foo(Args...) (otherwise previous clause fails)
    >::type
  >:std::true_type{};
}
template<class T, class Sig>
using has_foo = std::integral_constant<bool, details::has_foo<T, Sig>::value>;

which tries to invoke T.foo(int), and checks if the return value is compatible.

For fun, I made the type of has_foo actually be true_type or false_type, not inherited-from. I could just have:

template<class T, class Sig>
using has_foo = details::has_foo<T, Sig>;

if I didn't want that extra feature.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    I'm not sure I follow and if you don't mind explaining... why does it even has to guess anything here? `Args...` is clearly `int` here, no? [This works](http://coliru.stacked-crooked.com/a/d98b218acd9e0065) with, I think, exactly the same amount of information avaliable to the compiler. – jrok Oct 14 '14 at 14:55
  • Just like @Yakk explained, I only passed the first element of a parameter pack to a deduced function context, it's like a partial specialization and the compiler can't guarantee there isn't more things to deduce in that case, or a later specialization (to compare to object templates). – cdacamara Oct 14 '14 at 14:59
  • @jrok not sure. Maybe default parameters don't induce deduction in the same way? I figured out what was confusing the compiler from the error messages when I removed the `false_type` overload. – Yakk - Adam Nevraumont Oct 14 '14 at 15:04
  • @Yakk I think I'm getting my head wrapped around it now. And type deduction doesn't work at all with default arguments so I guess the compiler just takes what it gots there, seeing that `Args...` cannot be exapanded anymore? +1 Now, if you could also provide the standards quotes that govern this situation... :) – jrok Oct 14 '14 at 15:30
  • @Yakk The only problem I see with your new solution is there is no way to detect the if a member function is const qualified. For example if I just wanted `void foo() const` and not `void foo()` you can't differentiate between the two. – cdacamara Oct 14 '14 at 15:46
  • 1
    @cdacamara pass `const Foo` as the type. Note that it defaults to rvalues, include a `&` for lvalue overload detection. – Yakk - Adam Nevraumont Oct 14 '14 at 16:10