Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing if member function exists using variadics

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.

like image 431
cdacamara Avatar asked Oct 14 '14 14:10

cdacamara


1 Answers

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.

like image 197
Yakk - Adam Nevraumont Avatar answered Sep 28 '22 15:09

Yakk - Adam Nevraumont