Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are the type conversion rules for parameters and return values of lambdas?

Tags:

I was recently surprised by the fact that lambdas can be assigned to std::functions with slightly different signatures. Slightly different meaning that return values of the lambda might be ignored when the function is specified to return void, or that the parameters might be references in the function but values in the lambda.

See this example (ideone) where I highlighted what I would suspect to be incompatible. I would think that the return value isn't a problem since you can always call a function and ignore the return value, but the conversion from a reference to a value looks strange to me:

int main() {     function<void(const int& i)> f;     //       ^^^^ ^^^^^    ^     f = [](int i) -> int { cout<<i<<endl; return i; };     //     ^^^    ^^^^^^     f(2);     return 0; } 

The minor question is: Why does this code compile and work? The major question is: What are the general rules for type conversion of lambda parameters and return values when used together with std::function?

like image 657
anderas Avatar asked Jul 11 '16 08:07

anderas


People also ask

What is the return type of lambda?

The return type for a lambda is specified using a C++ feature named 'trailing return type'. This specification is optional. Without the trailing return type, the return type of the underlying function is effectively 'auto', and it is deduced from the type of the expressions in the body's return statements.

Can C++ lambda return value?

Return Value A C++ lambda function executes a single expression in C++. A value may or may not be returned by this expression. It also returns function objects using a lambda.

What is the type of lambda expression C++?

The type of a lambda expression is unspecified. But they are generally mere syntactic sugar for functors. A lambda is translated directly into a functor.

How do you pass a lambda function in C++?

Permalink. All the alternatives to passing a lambda by value actually capture a lambda's address, be it by const l-value reference, by non-const l-value reference, by universal reference, or by pointer.


2 Answers

You can assign a lambda to a function object of type std::function<R(ArgTypes...)> when the lambda is Lvalue-Callable for that signature. In turn, Lvalue-Callable is defined in terms of the INVOKE operation, which here means that the lambda has to be callable when it is an lvalue and all its arguments are values of the required types and value categories (as if each argument was the result of calling a nullary function with that argument type as the return type in its signature).

That is, if you give your lambda an id so that we can refer to its type,

auto l = [](int i) -> int { cout<<i<<endl; return i; }; 

To assign it to a function<void(const int&)>, the expression

static_cast<void>(std::declval<decltype(l)&>()(std::declval<const int&>())) 

must be well-formed.

The result of std::declval<const int&>() is an lvalue reference to const int, but there's no problem binding that to the int argument, since this is just the lvalue-to-rvalue conversion, which is considered an exact match for the purposes of overload resolution:

return l(static_cast<int const&>(int{})); 

As you've observed, return values are discarded if the function object signature has return type void; otherwise, the return types have to be implicitly convertible. As Jonathan Wakely points out, C++11 had unsatisfactory behavior on this (Using `std::function<void(...)>` to call non-void function; Is it illegal to invoke a std::function<void(Args...)> under the standard?), but it's been fixed since, in LWG 2420; the resolution was applied as a post-publication fix to C++14. Most modern C++ compiler will provide the C++14 behavior (as amended) as an extension, even in C++11 mode.

like image 146
ecatmur Avatar answered Sep 21 '22 21:09

ecatmur


the conversion from a reference to a value looks strange to me

Why?

Does this look strange too?

int foo(int i) { return i; }  void bar(const int& ir) { foo(ir); } 

This is exactly the same. A function taking an int by value gets called by another function, taking an int by const-reference.

Inside bar the variable ir gets copied, and the return value gets ignored. That's exactly what happens inside the std::function<void(const int&)> when it has a target with the signature int(int).

like image 24
Jonathan Wakely Avatar answered Sep 22 '22 21:09

Jonathan Wakely