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
:auto forwardingLambda = [](auto&& param) { /* perfect-forward param to f */ };Writing the perfect-forwarding call is easy, but it's probably not obvious how. The normal way to perfect-forward something it to use
std::forward
, so we'd expect to write essentially this: auto forwardingLambda = [](auto&& param) { f(std::forward<T>(param)); };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:auto forwardingLambda = [](auto&& param) { f(std::forward<std::conditional<std::is_rvalue_reference<decltype(param)>::value, std::remove_reference<decltype(param)>::type, decltype(param)>::type>(param)); };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: auto forwardingLambda = [](auto&& param) { f(std::forward<decltype(param)>(param)); };Frankly, this is more verbose than I'd prefer. One could imagine a world where you could say something like this:
auto forwardingLambda = [](<T1>&& param1, <T2>&& param2) { f(std::forward<T1>(param1), std::forward<T2>(param2)); };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 :-)
Scott
Thanks for an interesting post. +1 for the importance of the subject.
ReplyDeleteRegarding:
[](<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;} ?
PS. The parenthesis in the code utilising is_rvalue_reference needs reviewing. DS.
This form of perfect forwarding is inconsistent with my expectations. I would have expected:
ReplyDelete[](auto&& param) { std::forward(param); } // notice the auto&&
I would have expected param to need to be a universal reference.
I'll have to read the proposal for exactly how lambda auto parameters deduce type, but I expected a trivial 1:1 lowering to template(T ?? param) where ?? is the cv and reference types provided.
@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.
ReplyDelete@Anders Sjögren: As far as I know, there are no plans to offer "auto" parameters for non-template functions. Such functions would effectively be templates, anyway.
ReplyDeleteThanks for pointing out the parenthesis problem with my use of std::is_rvalue_reference. I've fixed it now (I hope!).
@Scott: Thanks for the clarification.
ReplyDeleteI 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.
> f(std::forward(param));
ReplyDeleteI'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: http://en.cppreference.com/w/cpp/language/decltype will tell you how decltype(entity) differs from decltype(expression). Thus decltype(x) != decltype((x)) in many cases.
ReplyDeleteThe following code highlights the behaviour in the case of template parameters and functions taking universal references ( http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers ):
enum class ref_type {none, lvalue, rvalue};
template<class T>
struct determine_ref_type
{
constexpr static ref_type value = std::is_rvalue_reference<T>::value ? ref_type::rvalue : (std::is_lvalue_reference<T>::value ? ref_type::lvalue : ref_type::none);
};
template<ref_type expected_decltype_ref_type, class T>
void assert_correct_reference_types(T&& x)
{
constexpr ref_type decltype_ref_type = determine_ref_type<decltype(x)>::value;
constexpr ref_type decltype_expr_ref_type = determine_ref_type<decltype((x))>::value;
static_assert(expected_decltype_ref_type == decltype_ref_type,"decltype of argument is of same type as passed in value");
static_assert(decltype_expr_ref_type == ref_type::lvalue,"decltype of expr is l-value ref");
}
void test_it()
{
int x = 1;
assert_correct_reference_types<ref_type::rvalue>(std::move(x));
assert_correct_reference_types<ref_type::rvalue>(1);
assert_correct_reference_types<ref_type::lvalue>(x);
}
@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.
ReplyDelete@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.
ReplyDeleteNow 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 :-)
ReplyDelete@Anders:
ReplyDelete> decltype(x) != decltype((x)) in many cases.
Thanks, this was exactly what was not clear enough to me!
@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).
ReplyDeleteBTW, I just noticed a (possible) typo:
...
We could thus write forwardingLambda like this:
auto forwardingLambda = [](auto&& param) {
f(std::forward::value,
std::remove_reference::type>(param));
};
That's 4 < and 3 >, if I still have my right eyesight ;-).
@Mark: Nice catch on the unbalanced angle brackets. I believe I've balanced them now, but I'm sure somebody will let me know if I'm mistaken!
ReplyDelete@scott Yes, those are great features to have but for completeness and to step up to the true generic power of named functors, we still need partially specialized lambdas. For instance,
ReplyDelete[](std::vector param1) { ... }
hmm, so blogger did not like my angle brackets. Here it is again, if anyone cares: [](std::vector<class T, class Alloc> & param1) { ... }
ReplyDeleteFWIW, GCC 4.9 supports the following syntax (proposed in N3560: Assorted Extensions to Lambda Expressions) which alleviates the forwarding problem (amongst others):
ReplyDelete[] <typename T> (T t) {}
E.g. Wrap a move-only function in a shared-ptr for storage in a std::function:
template <typename F, typename... A>
auto make_shared_callable(A&&... a)
{
return [pfn = std::make_shared<F>(std::forward<A>(a)...)]
<typename... B> (B&&... b) { return (*pfn)(std::forward<B>(b)...); };
}
@Adam Butcher: My understanding is that the only part of N3560 that was adopted was the provision for variadic parameters, so gcc's support for "<typename T>" is a nonstandard extension.
ReplyDeleteThanks for bringing this to my attention.
Yes that is my understanding also. Disappointing, but we have to accept the committee decisions I guess.
ReplyDeleteThe '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.
@Adam Butcher: Thanks for this additional information. I had not considered the equivalence of your two versions of add, but it's certainly interesting.
ReplyDelete