7

Context

(1) One can extract the return type and the argument types of a callable with the following trait:

#include <tuple>

template<class T>
struct callable_trait
{};

template<class R, class... Args>
struct callable_trait<R(Args...)>
{
    using return_type    = R;
    using argument_types = std::tuple<Args...>;
};

(2) Since C++17, one can define a template with template<auto>:

If a template parameter is declared auto, its type is deduced from the corresponding argument.

Question

(3) I should then be able to provide some syntactic sugar:

template<auto callable>
using return_type = typename callable_trait<decltype(callable)>::return_type;

... but it doesn't work too well ...

void f();
void g(return_type<f>);
error: no type named 'return_type' in 'callable_trait<void (*)()>'
using return_type = typename callable_trait<decltype(callable)>::return_type;
^~~~~

A lambda doesn't help ...

auto lambda= [](){};
void h(return_type<lambda>);
error: a non-type template parameter cannot have type '(lambda at <source>:19:14)'
void h(return_type<lambda>);
                   ^

Live demo

How can I circumvent this?

YSC
  • 38,212
  • 9
  • 96
  • 149
  • 1
    Lambda doesn't work with non-type template parameter. As long as you want to use `auto`, you can't circumvent it. Free function, on the other hand, works if you add a partial specialization for function pointers, i.e. `struct callable_trait` – felix Feb 22 '19 at 13:51

1 Answers1

10

In the function case the issue here is that decltype(callable) for a function returns a function pointer, which doesn't match your specialization. With the lambda, you get the type of the lambda, not it's operator(). You'll have the same problem if you use a member function as well since your specialization doesn't match a member function pointer.

What you need is something that can take all of those types and give you an R(Args...) in return. Thankfully we have std::function and it is built to do just this thing. It has deduction guides that will allow it to take any function type and make a std::function<R(Args...)> to match its signature. Using a std::function your code can become

template<class T>
struct callable_trait
{};

template<class R, class... Args>
struct callable_trait<std::function<R(Args...)>>
{
    using return_type    = R;
    using argument_types = std::tuple<Args...>;
    static constexpr size_t argument_count = sizeof...(Args);
};

template<auto callable>
using return_type = typename callable_trait<decltype(std::function{callable})>::return_type;

template<auto callable>
static constexpr size_t argument_count = callable_trait<decltype(std::function{callable})>::argument_count;

void f();
void g(return_type<f>);

auto lambda = [](){};
void h(return_type<lambda>);

void e(int, int, int);
static_assert(argument_count<e> == 3, "oh no");

but this only works on gcc head. Clang can't deduce the std::function and earlier versions of gcc and MSVS fail for the reason detailed here: Why is gcc failing when using lambda for non-type template parameter?

If you switch to taking a type parameter and use decltype is works on both gcc and MSVS but clang still has problems with the deduction guide

template<class T>
struct callable_trait
{};

template<class R, class... Args>
struct callable_trait<std::function<R(Args...)>>
{
    using return_type    = R;
    using argument_types = std::tuple<Args...>;
    static constexpr size_t argument_count = sizeof...(Args);
};

template<typename callable>
using return_type = typename callable_trait<decltype(std::function{std::declval<callable>()})>::return_type;

template<typename callable>
static constexpr size_t argument_count = callable_trait<decltype(std::function{std::declval<callable>()})>::argument_count;

void f();
void g(return_type<decltype(f)>);

auto lambda = [](){};
void h(return_type<decltype(lambda)>);

void e(int, int, int);
static_assert(argument_count<decltype(e)> == 3, "oh no");
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • nice explanation and elaburation! – Const Feb 22 '19 at 16:56
  • I'm probably missing how this happens. `function` is a template class. I can't see how you'd be able to construct it without providing those arguments. When I try to do this I get: "error C2955: `std::function`: use of class template requires template argument list" – Jonathan Mee Mar 04 '19 at 16:11
  • @jonathanmee C++17 gives us class template argument deduction so you don't have to specify the template parameters anymore. std::function has a deduction guides that figures it out for us. This is a really nice feature and I'm using it whenever I can. – NathanOliver Mar 04 '19 at 16:28
  • Yeah I'm stuck on Visual Studio 15.6.7 which doesn't appear to support class template argument deduction T.T Is there any possibility of a generic workaround? – Jonathan Mee Mar 04 '19 at 16:32
  • 1
    @JonathanMee I'm not aware of one. You would have to specialize `callable_trait` for each type of callable (function pointers, functors, member function pointers). – NathanOliver Mar 04 '19 at 19:33
  • @NathanOliverI Actually asked a question about it: https://stackoverflow.com/q/54990207/2642059 Seems like you're right :( – Jonathan Mee Mar 04 '19 at 20:32