Monday, May 6, 2013

C++14 Lambdas and Perfect Forwarding

So the joke's on me, I guess.

In my discussion of std::move vs. std::forward, I explained that when you call std::forward, the expectation is that you'll pass a type consistent with the rules for template type deduction, meaning (1) an lvalue reference type for lvalues and (2) a non-reference type for rvalues. I added,

If you decide to be a smart aleck and write [code passing an rvalue reference type], the reference-collapsing rules will see that you get the same behavior as [you would passing a non-reference type], but with any luck, your team lead will shift you to development in
straight C, where you'll have to content yourself with writing bizarre macros.

Well. As I said, the joke seems to be on me, because the standardization commitee apparently consists largely of smart alecks.

Let me explain.

The recently-adopted C++14 CD includes beefy additions to lambda capabilities, including the support for polymorphic lambdas that Herb Sutter can't help but mention I've been whining about for years. This means that in C++14, we now have the expressive power that the Boost Lambda library has been offering since 2002. Ahem. But C++14 goes further, supporting also variadic lambdas, generalized captures (including capture-by-move), and, of particular relevance to this post, support for perfect forwarding.

Suppose we want to write a C++14 lambda that takes a parameter and perfect-forwards it to some function f:

But, uh oh, there's no T to pass to std::forward. (In the class generated from the lambda expression, there is, but inside the lambda itself, there's no type for param.) So what do we pass to std::forward? We can hardly pass auto. (Consider what would happen if we had a lambda taking multiple parameters, each of type auto and each of which we wanted to forward. In that case, each std::forward<auto> would be ambiguous: which auto should std::forward use?)

The solution takes advantage of two observations. First, the type-deduction rules for auto in lambdas are the same as for templates. This means that if an lvalue argument is passed to the lambda, param's type will be an lvalue reference--exactly what we need for std::forward. If an rvalue argument is passed, its type will be an rvalue reference. For such parameters, we can recover the type to pass to std::forward by stripping it of its reference-ness. We could thus write forwardingLambda like this:

At least I think we could. I don't have a C++14 compiler to try it with, and, anyway, it's too gross to waste time on. It would be sad, indeed, if this is what the standardization committee expected us to do to effect perfect forwarding inside its spiffy new C++14 lambdas. Fortunately, it doesn't.

Which brings us to observation number two. As I noted near the beginning of this post,

If you decide to be a smart aleck and write [code passing an rvalue reference type to std::forward], the reference-collapsing rules will see that you get the same behavior as [you would passing a non-reference type].

That means that if param's type is an rvalue reference, there is no need to strip off its reference-ocity. Instead, you can smart aleck your way to success by simply passing that type directly to std::forward. Like so:

But that's not the world we live in, and given that C++14 gives us polymorphic lambdas, variadic lambdas, and move-enabled lambdas, I'm not going to complain about the world of C++14 lambdas. Except possibly to Herb :-)

19 comments:

Regarding:[](<T1> param1, <T2> param2) { f(std::forward<T1>(param1), std::forward<T2>(param2)); };Wouldn't one option be to allow:template<class T1, class T2>[](T1 param1, T2 param2) { f(std::forward<T1>(param1), std::forward<T2>(param2)); };While that is more verbose, it is also more in line with regular template code.

I find one of the problems with C++ to be that there are just too many language tricks and quirks, and adding the syntax "<T1> param1" would create yet another one. On the other hand, it is kind of neat...

Also, is there some plan of adding the same functionality for regular functions, e.g. to allow int f(auto x){return x;} ?

@mmocny: Your expectation regarding the need for "&&" after "auto" is correct, I simply forgot to put it in. I've updated the post to correct this. Your expectation about a 1:1 mapping from "auto" in a lambda to a type parameter in the underlying implementation is also correct. Full details are available in N3649, the proposal that was adopted.

@Scott: Thanks for the clarification. I guess a reason for allowing auto in regular function definitions would have been brevity, and consistency in syntax when defining different callable artifacts. It seems one or more of the holes mended by c++14 have to do with lack of completeness and consistency between functions and lambdas, e.g. in the case of return type deduction, and with why-can't-I:s, like multiple returns in return type deductions. In general I guess the language gets easier to learn and to read if the same coding style is allowed and used in similar situations, and that it just feels more coherent and complete. It seems like a good thing to have less rules where you have to know in what specific circumstances you may use one form of syntax or another. It feels like lambdas ideally would be equivalent in syntax to a regular function definition, expect that one starts with [] and the other with auto function_name, but that the rest of the definition rules should be identical (including a starting template<...> block). Just my 5c, being fairly ignorant of all the devils lurking in the details.

I'm surprised decltype can extract l/r-valueness. Wasn't the "rule" that a parameter is always an l-value in the body of a function?Or does one have to distinguish between l/r-valueness of type and expression?I'm probably overlooking some subtleness here, but: if l/r-valueness is part of the type, why does std::forward require a template argument and not deduce it from the parameter type?I thought I understood l/r-valueness until seeing "decltype< param >"(param)...

@Zenju: decltype can detect whether an expression is an lvalue or an rvalue, but in its use with std::forward in lambdas, that's not what it's doing. Parameters are always lvalues. What decltype is doing in that case is returning the type of the parameter, which, because it's declared to be of type auto&& (i.e., a universal reference), will always be either an lvalue reference or an rvalue reference. This means that std::forward receives either an lvalue reference type or an rvalue reference type. Due to the rules for reference collapsing, std::forward's behavior when passed an rvalue reference type is the same as when it's passed a non-reference type, and passing a non-reference type to std::forward is the convention for indicating that the parameter to which std::forward is applied should be treated as an rvalue. The explanation is somewhat confusing, because the underlying mechanics are complicated, but the end result is that std::forward<decltype(param)>(param) does what it's supposed to: casts param to an rvalue only if the argument passed to param was an rvalue.

@scott @zenju I was curious if this decltype behaviour is specific to lambda (which would have been suprising), but it thankfully is not. I have a test case working with existing compilers using normal function templates such that decltype(param) with forward works as scott explains here.

Now I wonder if that means a FORWARD(param) macro can be written, and if it would work for all cases..

@mmocny: There is nothing special about decltype (or anything else) inside a lambda expression, so a FORWARD macro such as you suggest could be written, and I can't think of any reason it would not work in all "normal" cases. Whether it would work in absolutely all cases, I'm not sure. The rules for type deduction used by decltype are not identical to the rules for type deduction used by function templates, so there might be some edge cases where FORWARD would not yield the behavior you'd expect. Or there might not be :-)

@Scott: Thanks for the "walkthrough". Each step makes sense, yet it's sometimes hard to see the bigger picture. Maybe it's because l/r-valueness is not visible in the source code but has to be deduced by rules, that makes it a difficult concept. I guess it's this and the fact that it's context dependent unlike the "plain types" one is used to prior to C++11.

Nice and interesting post. Good thing there's decltype to provide a work-around to facilitate those new C++14 lambda features. I wonder what additional syntax for lambdas would the standard committee create (if they decide to do so).

Yes that is my understanding also. Disappointing, but we have to accept the committee decisions I guess.

The 'familiar' template syntax seemed like a natural thing to me when I first made the original lambda branch patch back in 2009. When I got some free time in July this year and began the implementation proper in GCC 4.9, the explicit syntax was the first thing I did as it allowed to implement generic lambdas without implicit function templates (i.e. without handling 'auto' type parameters).

Incidentally, as well as the explicit template syntax extension, GCC 4.9 also supports implicit function templates through the use of 'auto' parameters in 'normal' function declarations. E.g.

auto add(auto a, auto b) { return a + b; }

is accepted. My intention was to provide a route toward the terse syntax of concepts lite but I haven't had the time to try it out on that branch yet. I see no reason to prohibit the intuitive example above when it can be arrived at more obtusely with standard C++14 as:

auto add = [] (auto a, auto b) { return a + b; };

Sorry for the digression into implicit function templates but it was mentioned earlier in the comments above.

I just hope that the Concepts Lite TS will provide C++ with a clean regular syntax for these things such that it is easier to teach and learn.