Is it possible to figure out the parameter type and return type of a lambda?
Asked Answered
D

6

147

Given a lambda, is it possible to figure out it's parameter type and return type? If yes, how?

Basically, I want lambda_traits which can be used in following ways:

auto lambda = [](int i) { return long(i*10); };

lambda_traits<decltype(lambda)>::param_type  i; //i should be int
lambda_traits<decltype(lambda)>::return_type l; //l should be long

The motivation behind is that I want to use lambda_traits in a function template which accepts a lambda as argument, and I need to know it's parameter type and return type inside the function:

template<typename TLambda>
void f(TLambda lambda)
{
   typedef typename lambda_traits<TLambda>::param_type  P;
   typedef typename lambda_traits<TLambda>::return_type R;

   std::function<R(P)> fun = lambda; //I want to do this!
   //...
}

For the time being, we can assume that the lambda takes exactly one argument.

Initially, I tried to work with std::function as:

template<typename T>
A<T> f(std::function<bool(T)> fun)
{
   return A<T>(fun);
}

f([](int){return true;}); //error

But it obviously would give error. So I changed it to TLambda version of the function template and want to construct the std::function object inside the function (as shown above).

Demmer answered 30/10, 2011 at 5:47 Comment(7)
If you know the parameter type then this can be used to figure out the return type. I don't know how to figure out the parameter type though.Exaggeration
Is it assumed that function takes single argument ?Thrombocyte
"parameter type" But an arbitrary lambda function doesn't have a parameter type. It could take any number of parameters. So any traits class would have to be designed to query parameters by position indices.Osteology
@iammilind: Yes. for the time being, we can assume that.Demmer
@NicolBolas: For the time being, we can assume that the lambda takes exactly one argument.Demmer
"The motivation behind is that I want to use lambda_traits in a function template which accepts a lambda as argument" Why would you want it to only accept a lambda? Wouldn't it make more sense to just use a std::function? Do you really want to force the user to only ever call this with specifically a lambda functor?Osteology
@NicolBolas: Because std::function doesn't seem to work: ideone.com/E1IqfDemmer
P
181

Funny, I've just written a function_traits implementation based on Specializing a template on a lambda in C++0x which can give the parameter types. The trick, as described in the answer in that question, is to use the decltype of the lambda's operator().

template <typename T>
struct function_traits
    : public function_traits<decltype(&T::operator())>
{};
// For generic types, directly use the result of the signature of its 'operator()'

template <typename ClassType, typename ReturnType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const>
// we specialize for pointers to member function
{
    enum { arity = sizeof...(Args) };
    // arity is the number of arguments.

    typedef ReturnType result_type;

    template <size_t i>
    struct arg
    {
        typedef typename std::tuple_element<i, std::tuple<Args...>>::type type;
        // the i-th argument is equivalent to the i-th tuple element of a tuple
        // composed of those arguments.
    };
};

// test code below:
int main()
{
    auto lambda = [](int i) { return long(i*10); };

    typedef function_traits<decltype(lambda)> traits;

    static_assert(std::is_same<long, traits::result_type>::value, "err");
    static_assert(std::is_same<int, traits::arg<0>::type>::value, "err");

    return 0;
}

Note that this solution does not work for generic lambda like [](auto x) {}.

Penholder answered 30/10, 2011 at 7:27 Comment(18)
Heh, I was just writing this. Didn't think about tuple_element though, thanks.Coincidentally
@GMan: If your approach is not exactly same as this, please post it then. I'm going to test this solution.Demmer
@Nawaz: The utility Kenny linked to is nearly identical to my own utility I have for my projects. (I just have to Boost.PP iterate since I'm stuck in MSVC C++0x support.) The only thing is I wrote a get_nth_element meta-function, instead of (ab)using tuple_element.Coincidentally
A complete trait would also use a specialization for non-const, for those lambda declared mutable ([]() mutable -> T { ... }).Duchess
Try the function_traits specialization first and then #include <random>. The compiler goes bonkers.Timmy
It might also be useful to include typedef ReturnType(simple_function_type)(Args...) into the function_traits class. And also, perhaps typedef std::function<simple_function_type> std_function_type;Fante
A follow up question here #48079218Prosciutto
This does not work in case where a lambda has auto arguments: [](auto i). The error states for msvc2015: error C3556: 'ttt::<lambda_ff3930f11af2faf2c0b2ac7ad388c762>::operator ()': incorrect argument to 'decltype' or clang4.0: reference to overloaded function could not be resolved. I know this is legit error, but even so the arity is not reachable because of the error.Bellarmine
@Bellarmine that's a fundamental problem with function objects that have (potentially) multiple overloads of operator() not with this implementation. auto is not a type, so it can't ever be the answer to traits::template arg<0>::typeSynchronize
@Bellarmine e.g. what is the arity of struct functor { void operator()(int) {}; void operator()(int, double, bool) {} };?Synchronize
@Synchronize I agree, in overloaded case you can not find the arity. But in case of lambda [](auto i) the arity is known, because you have single operator with templated arguments and you still can not get it's quantity. And even more, you have to check the object on callability, before access the arity, so described traits implementation is not enough in this case.Bellarmine
@Bellarmine that's overloaded, in the general caseSynchronize
@Synchronize Templated != OverloadedBellarmine
You can use that lambda in multiple contexts and i will be deduced as multiple types. The class synthesised will have multiple overloads of operator(). I don't know of any method of finding out if all the overloads of a given function have the same arity, which is what you would need.Synchronize
Why it should have multiple overloads?Bellarmine
Let us continue this discussion in chat.Synchronize
@Bellarmine Page is gone... Got a spare?Brawley
if somebody interested in better implementation: github.com/andry81/tacklelib/tree/HEAD/include/tacklelib/…Bellarmine
H
12

Though I'm not sure this is strictly standard conforming, ideone compiled the following code:

template< class > struct mem_type;

template< class C, class T > struct mem_type< T C::* > {
  typedef T type;
};

template< class T > struct lambda_func_type {
  typedef typename mem_type< decltype( &T::operator() ) >::type type;
};

int main() {
  auto l = [](int i) { return long(i); };
  typedef lambda_func_type< decltype(l) >::type T;
  static_assert( std::is_same< T, long( int )const >::value, "" );
}

However, this provides only the function type, so the result and parameter types have to be extracted from it. If you can use boost::function_traits, result_type and arg1_type will meet the purpose. Since ideone seems not to provide boost in C++11 mode, I couldn't post the actual code, sorry.

Haw answered 30/10, 2011 at 7:17 Comment(1)
I think, it is a good start. +1 for that. Now we need to work on function type to extract the required information. (I don't want to use Boost as of now, as I want to learn the stuffs).Demmer
O
9

The specialization method shown in @KennyTMs answer can be extended to cover all cases, including variadic and mutable lambdas:

template <typename T>
struct closure_traits : closure_traits<decltype(&T::operator())> {};

#define REM_CTOR(...) __VA_ARGS__
#define SPEC(cv, var, is_var)                                              \
template <typename C, typename R, typename... Args>                        \
struct closure_traits<R (C::*) (Args... REM_CTOR var) cv>                  \
{                                                                          \
    using arity = std::integral_constant<std::size_t, sizeof...(Args) >;   \
    using is_variadic = std::integral_constant<bool, is_var>;              \
    using is_const    = std::is_const<int cv>;                             \
                                                                           \
    using result_type = R;                                                 \
                                                                           \
    template <std::size_t i>                                               \
    using arg = typename std::tuple_element<i, std::tuple<Args...>>::type; \
};

SPEC(const, (,...), 1)
SPEC(const, (), 0)
SPEC(, (,...), 1)
SPEC(, (), 0)

Demo.

Note that the arity is not adjusted for variadic operator()s. Instead one can also consider is_variadic.

Operable answered 29/1, 2015 at 11:35 Comment(0)
A
2

The answer provided by @KennyTMs works great, however if a lambda has no parameters, using the index arg<0> does not compile. If anyone else was having this problem, I have a simple solution (simpler than using SFINAE related solutions, that is).

Just add void to the end of the tuple in the arg struct after the variadic argument types. i.e.

template <size_t i>
    struct arg
    {
        typedef typename std::tuple_element<i, std::tuple<Args...,void>>::type type;
    };

since the arity isn't dependent on the actual number of template parameters, the actual won't be incorrect, and if it's 0 then at least arg<0> will still exist and you can do with it what you will. If you already plan to not exceed the index arg<arity-1> then it shouldn't interfere with your current implementation.

Accord answered 10/5, 2018 at 10:28 Comment(0)
D
2

Here is my toy version of function traits. I took advantage of std::function's type inference without actually building any std::function object.

This allows working with regular functions, not just functors.


template <typename Return, typename... Args>
struct FunctionTraits_ {
  FunctionTraits_(std::function<Return(Args...)> f) {}
  using return_type = Return;
  using arg_types = std::tuple<Args...>;

  enum { arity = sizeof...(Args) };
  template <size_t i>
  struct arg_type {
    using type = typename std::tuple_element<i, arg_types>::type;
  };
};

template <typename Function>
struct FunctionTraits : public decltype(FunctionTraits_(
                            std::function{std::declval<Function>()})) {};

// Testing
std::size_t func(int, const std::string &s) { return s.size(); }
auto lambda = [](int, const std::string &s) { return s.size(); };

int main() {
  using traits = FunctionTraits<decltype(func)>;
  // using traits = FunctionTraits<decltype(lambda)>;

  static_assert(traits::arity == 2);
  static_assert(std::is_same_v<traits::return_type, std::size_t>);
  static_assert(std::is_same_v<traits::arg_type<0>::type, int>);
  static_assert(std::is_same_v<traits::arg_type<1>::type, const std::string &>);
  static_assert(std::is_same_v<traits::arg_types, std::tuple<int, const std::string &>>);

  return 0;
}

Thanks, Cœur, for the inspiration.

Diastema answered 24/12, 2023 at 19:44 Comment(0)
S
0

If you're looking for a complete solution for all types in C++ that can be invoked, a lot of these answers work but miss some of the corner cases, like

  • A reference to a lambda
  • Functions and function pointers

Here's a complete solution to my knowledge (except for generic lambdas) - let me know in the comments if anything is missing:

template <typename>
struct closure_traits;

template <typename FunctionT> // overloaded operator () (e.g. std::function)
struct closure_traits
    : closure_traits<decltype(&std::remove_reference_t<FunctionT>::operator())>
{
};

template <typename ReturnTypeT, typename... Args> // Free functions
struct closure_traits<ReturnTypeT(Args...)>
{
    using arguments = std::tuple<Args...>;

    static constexpr std::size_t arity = std::tuple_size<arguments>::value;

    template <std::size_t N>
    using argument_type = typename std::tuple_element<N, arguments>::type;

    using return_type = ReturnTypeT;
};

template <typename ReturnTypeT, typename... Args> // Function pointers
struct closure_traits<ReturnTypeT (*)(Args...)>
    : closure_traits<ReturnTypeT(Args...)>
{
};

// member functions
template <typename ReturnTypeT, typename ClassTypeT, typename... Args>
struct closure_traits<ReturnTypeT (ClassTypeT::*)(Args...)>
    : closure_traits<ReturnTypeT(Args...)>
{
    using class_type = ClassTypeT;
};

// const member functions (and lambda's operator() gets redirected here)
template <typename ReturnTypeT, typename ClassTypeT, typename... Args>
struct closure_traits<ReturnTypeT (ClassTypeT::*)(Args...) const>
    : closure_traits<ReturnTypeT (ClassTypeT::*)(Args...)>
{
};

Disclaimer: the std::remove_reference was inspired by this code.

Squatter answered 21/6, 2022 at 2:21 Comment(2)
Covers most mainstream scenarios but not a complete solution (see my own production grade, fully documented solution at github.com/HexadigmAdmin/FunctionTraits). You're missing (in no particular order) specializations that handle noexcept (part of a function's type since C++17 - this is a must-have), "volatile" cv-qualifier on non-static member functions (though very rare in the real world), ref-qualifiers on member functions (again, very rare), variadic functions (those with "..." args), and calling conventions (only the default will be detected - "cdecl" usually).Flaw
Also, function pointers with cv-qualifiers won't be detected (caller has to invoke "std::remove_cv_t" first - not a showstopper but a bit inconvenient)Flaw

© 2022 - 2024 — McMap. All rights reserved.