P0756R0: "Lambda syntax should be more liberal in what it accepts"

290 views
Skip to first unread message

Arthur O'Dwyer

unread,
Jul 17, 2017, 2:46:38 PM7/17/17
to ISO C++ Standard - Future Proposals
This paper proposes to remove two minor stumbling blocks to the teachability of lambda syntax — stumbling blocks of which many programmers may not even be aware.
The first is that [&x, =](){ return x; } is ill-formed.
The second is that [=, y](){ return x; } is ill-formed.
I propose to make both of these lambdas well-formed, with the obvious meanings.

http://quuxplusone.github.io/draft/liberal-syntax-for-lambdas.html

Comments welcome, either here or via email. Remember to read the paper first, though!

–Arthur

Nicol Bolas

unread,
Jul 17, 2017, 7:00:00 PM7/17/17
to ISO C++ Standard - Future Proposals

> placing the = at the end of the list (where it logically belongs)

I strongly disagree with the parenthetical statement. Seeing `=` first makes it clear to the reader that this is a value default-capturing lambda. Seeing `&` first makes it clear that it's a reference default-capturing lambda. Seeing neither makes it clear that it captures only what is listed.

It's a simple rule to remember: put the default first. I don't think that rule affects teachability.

The reason why you needed that paragraph is because you taught it wrong. Your tutorial said "To capture "everything needed by this lambda, and nothing else," simply add a single = to your capture-list (to capture by value), or add a single & (to capture by reference)." That's incorrect. The correct phrase is "To capture "everything needed by this lambda, and nothing else, simply put = (to capture by value), or & (to capture by reference) at the beginning of your capture list."

Your mistake is not the standard's fault.

As for making redundancy illegal, I believe that if you write something redundant, then you've clearly made some kind of mistake. If you use a default capture, and you then capture a variable in the same way as the default capture, then you are perhaps confused as to what you meant to capture and how you meant to capture it.

Yes, you can now use capture expressions to get the same effect without getting an error. But I don't think that represents a problem.

> improve our friendliness to machine-generated or mechanically-refactored code.

OK, this is a much better point for the forbidding redundancy issue. Even so, I'd like to know a bit more about why the machine generated code would think that it needs to value-capture everything, while another part of the machine generated code needs to value capture a specific variable. These two thoughts don't seem to be in sync; it seems more likely that we'd be in a world where the user (the one providing some of the code that goes in the lambda) specified either the default capture or the specific variable capture, and the machine system specified the other.

But that still doesn't justify allowing default-capture after the start of a lambda.

Barry Revzin

unread,
Jul 18, 2017, 12:19:37 PM7/18/17
to ISO C++ Standard - Future Proposals
As for making redundancy illegal, I believe that if you write something redundant, then you've clearly made some kind of mistake. If you use a default capture, and you then capture a variable in the same way as the default capture, then you are perhaps confused as to what you meant to capture and how you meant to capture it.


I think the problem OP is trying to solve is that he wants to capture a variable by-copy that isn't actually odr-used (or really, used by any definition) in the lambda. So [=] won't capture it, but [=, token] is ill-formed. 

Nicol Bolas

unread,
Jul 18, 2017, 1:01:35 PM7/18/17
to ISO C++ Standard - Future Proposals
On Tuesday, July 18, 2017 at 12:19:37 PM UTC-4, Barry Revzin wrote:
As for making redundancy illegal, I believe that if you write something redundant, then you've clearly made some kind of mistake. If you use a default capture, and you then capture a variable in the same way as the default capture, then you are perhaps confused as to what you meant to capture and how you meant to capture it.


I think the problem OP is trying to solve is that he wants to capture a variable by-copy that isn't actually odr-used (or really, used by any definition) in the lambda. So [=] won't capture it, but [=, token] is ill-formed.

In that case, I think the `x=x` form would make it more clear what you're trying to do. It demonstrates that you're not merely capturing the variable for use in the lambda; you're explicitly and deliberately copying it into the members of the object.

Arthur O'Dwyer

unread,
Jul 18, 2017, 1:26:49 PM7/18/17
to ISO C++ Standard - Future Proposals
Yep, that's covered in the paper.

In re:
> It's a simple rule to remember: put the default first. I don't think that rule affects teachability.

I'd like to add that sentence to the paper, somewhere, so that I can rebut it.
Currently C++ supports "defaults" or "default-like behavior" in the following places that I'm aware of:
- Default-valued function arguments (only at the end)
- C-style variadic "do this with all the unmatched arguments" (only at the end)
- Variadic template parameter packs (anywhere, but typically you want to put them at the end)
- Aggregate initializer "set all other fields to zero" (only at the end)
- Default-valued template parameters (anywhere)
- Non-static data members (anywhere)
- Lambda captures (only at the beginning)

Am I missing anyplace in C++ that does follow the simple rule "put the default first"?  It seems to me that that "rule" is more of a special case that changes the general rule only in the case of lambda-captures — which is why I'm proposing to change it, to bring capture-lists into closer harmony with the rest of the language.

In many of the above cases, there's a grammatical reason that they can appear only at the end of the construct. For the cases where there's no grammatical reason to put them at the end, C++ allows you to put them anywhere. — and then there's lambda-captures, which currently work "backwards" from the rest of the language.

–Arthur

Nicol Bolas

unread,
Jul 18, 2017, 1:41:03 PM7/18/17
to ISO C++ Standard - Future Proposals
On Tuesday, July 18, 2017 at 1:26:49 PM UTC-4, Arthur O'Dwyer wrote:
On Tue, Jul 18, 2017 at 10:01 AM, Nicol Bolas <jmck...@gmail.com> wrote:
On Tuesday, July 18, 2017 at 12:19:37 PM UTC-4, Barry Revzin wrote:
As for making redundancy illegal, I believe that if you write something redundant, then you've clearly made some kind of mistake. If you use a default capture, and you then capture a variable in the same way as the default capture, then you are perhaps confused as to what you meant to capture and how you meant to capture it.

I think the problem OP is trying to solve is that he wants to capture a variable by-copy that isn't actually odr-used (or really, used by any definition) in the lambda. So [=] won't capture it, but [=, token] is ill-formed.

In that case, I think the `x=x` form would make it more clear what you're trying to do. It demonstrates that you're not merely capturing the variable for use in the lambda; you're explicitly and deliberately copying it into the members of the object.

Yep, that's covered in the paper.

In re:
> It's a simple rule to remember: put the default first. I don't think that rule affects teachability.

I'd like to add that sentence to the paper, somewhere, so that I can rebut it.
Currently C++ supports "defaults" or "default-like behavior" in the following places that I'm aware of:
- Default-valued function arguments (only at the end)
- C-style variadic "do this with all the unmatched arguments" (only at the end)
- Variadic template parameter packs (anywhere, but typically you want to put them at the end)
- Aggregate initializer "set all other fields to zero" (only at the end)
- Default-valued template parameters (anywhere)
- Non-static data members (anywhere)
- Lambda captures (only at the beginning)


Notably, most of those are about values: initializing a particular named construct with a particular value. The ones that aren't are the variadics.

Specifying a default capture for a lambda is not assigning a named construct a particular value. So there's no reason to expect them to work the same way. Just because they use the same word "default" does not mean they're somehow comparable.

Unless you think that "default initialization" is somehow related to and should be syntactically consistent with "default parameters" or "default capture".
 
Am I missing anyplace in C++ that does follow the simple rule "put the default first"?  It seems to me that that "rule" is more of a special case that changes the general rule only in the case of lambda-captures — which is why I'm proposing to change it, to bring capture-lists into closer harmony with the rest of the language.

To what end? You're making it harder for a user to find how the lambda captures values by default. "Consistency" (even if we ignore the above, where we show there's no expectation of consistency) is not as important as usability.

Think of it like this. If C++ had more available syntax, the default capture style would be part of the lambda inducer's syntax itself. We only put it inside the inducer because it's syntactically easier.

Greg Marr

unread,
Aug 12, 2017, 10:36:58 PM8/12/17
to ISO C++ Standard - Future Proposals
On Tuesday, July 18, 2017 at 1:26:49 PM UTC-4, Arthur O'Dwyer wrote:
In many of the above cases, there's a grammatical reason that they can appear only at the end of the construct. For the cases where there's no grammatical reason to put them at the end, C++ allows you to put them anywhere. — and then there's lambda-captures, which currently work "backwards" from the rest of the language.

With the default first, the grammar can enforce that it's only used once, and that only one of them is used.  With it allowed anywhere, as in your paper, it can't be enforced by the grammar. so you had to add the separate rule to spell out that restriction.

Arthur O'Dwyer

unread,
Aug 13, 2017, 12:52:24 AM8/13/17
to ISO C++ Standard - Future Proposals
Yup. But I was able to strike out a whole paragraph of other rules that can't be enforced by the grammar! :)
I've finally gotten around to updating the paper to add a section attempting to rebut the "default always goes first" argument.

Does anyone happen to know the exact date of the pre-Albuquerque mailing submission deadline?  I was hoping to see it mentioned in N4633 but nope.

Thanks,
–Arthur

Tom Honermann

unread,
Aug 13, 2017, 9:54:10 AM8/13/17
to std-pr...@isocpp.org

On Aug 13, 2017, at 12:52 AM, Arthur O'Dwyer <arthur....@gmail.com> wrote:

Does anyone happen to know the exact date of the pre-Albuquerque mailing submission deadline?  I was hoping to see it mentioned in N4633 but nope.

October 16th. 

Tom. 

Avi Kivity

unread,
Aug 15, 2017, 2:29:27 PM8/15/17
to std-pr...@isocpp.org, Arthur O'Dwyer
I would like to re-raise [&&x] as a shortcut for [x = std::move(x)] () mutable.  It is seen extensively in continuation passing code, and since the language has first-class support for moves, lambdas should too.

Nicol Bolas

unread,
Aug 15, 2017, 3:03:32 PM8/15/17
to ISO C++ Standard - Future Proposals, arthur....@gmail.com

One could argue for the `&&x -> x = std::move(x)` part. But the automatic `mutable`-izing part? No. Whether you agree with the `const`-by-default nature of lambdas or not, we shouldn't create syntax that arbitrarily changes the nature of a lambda.

Also, do you really want to move from `x`? Or do you want to do a decay-copy into `x`? Because the latter is what `std::thread/async` do. And we already have a proposal for shortening that.

Avi Kivity

unread,
Aug 16, 2017, 4:47:32 AM8/16/17
to std-pr...@isocpp.org, Nicol Bolas, arthur....@gmail.com



On 08/15/2017 10:03 PM, Nicol Bolas wrote:
On Tuesday, August 15, 2017 at 2:29:27 PM UTC-4, Avi Kivity wrote:
On 07/17/2017 09:46 PM, Arthur O'Dwyer wrote:
This paper proposes to remove two minor stumbling blocks to the teachability of lambda syntax — stumbling blocks of which many programmers may not even be aware.
The first is that [&x, =](){ return x; } is ill-formed.
The second is that [=, y](){ return x; } is ill-formed.
I propose to make both of these lambdas well-formed, with the obvious meanings.

http://quuxplusone.github.io/draft/liberal-syntax-for-lambdas.html

Comments welcome, either here or via email. Remember to read the paper first, though!


I would like to re-raise [&&x] as a shortcut for [x = std::move(x)] () mutable.  It is seen extensively in continuation passing code, and since the language has first-class support for moves, lambdas should too.

One could argue for the `&&x -> x = std::move(x)` part. But the automatic `mutable`-izing part? No. Whether you agree with the `const`-by-default nature of lambdas or not, we shouldn't create syntax that arbitrarily changes the nature of a lambda.

I see your point. How about, &&x translates to x = std::move(x) _and_ x is a mutable member of the synthetic struct, even if `mutable` is not specified?



Also, do you really want to move from `x`?

I really do want to move from x. I have thousands of lines doing that, though I expect a significant reduction when we start using coroutines.


Or do you want to do a decay-copy into `x`? Because the latter is what `std::thread/async` do. And we already have a proposal for shortening that.


This is wonderful. How about, in addition, a unary &&?   &&x == std::move(x).  std::move(x) is so common it deserves some syntax.


Nicol Bolas

unread,
Aug 16, 2017, 11:08:01 AM8/16/17
to ISO C++ Standard - Future Proposals, jmck...@gmail.com, arthur....@gmail.com
On Wednesday, August 16, 2017 at 4:47:32 AM UTC-4, Avi Kivity wrote:
On 08/15/2017 10:03 PM, Nicol Bolas wrote:
On Tuesday, August 15, 2017 at 2:29:27 PM UTC-4, Avi Kivity wrote:
On 07/17/2017 09:46 PM, Arthur O'Dwyer wrote:
This paper proposes to remove two minor stumbling blocks to the teachability of lambda syntax — stumbling blocks of which many programmers may not even be aware.
The first is that [&x, =](){ return x; } is ill-formed.
The second is that [=, y](){ return x; } is ill-formed.
I propose to make both of these lambdas well-formed, with the obvious meanings.

http://quuxplusone.github.io/draft/liberal-syntax-for-lambdas.html

Comments welcome, either here or via email. Remember to read the paper first, though!


I would like to re-raise [&&x] as a shortcut for [x = std::move(x)] () mutable.  It is seen extensively in continuation passing code, and since the language has first-class support for moves, lambdas should too.

One could argue for the `&&x -> x = std::move(x)` part. But the automatic `mutable`-izing part? No. Whether you agree with the `const`-by-default nature of lambdas or not, we shouldn't create syntax that arbitrarily changes the nature of a lambda.

I see your point. How about, &&x translates to x = std::move(x) _and_ x is a mutable member of the synthetic struct, even if `mutable` is not specified?

No. There's no reason to implicitly assume that, if a user wants to move an object into the lambda, they also mean to modify it. These are two orthogonal concepts.

If you move a `unique_ptr` into a lambda, that doesn't mean you're going to modify the `unique_ptr` *itself*. You may modify what it points to, but `unique_ptr` doesn't forward `const` through to `get`.
Also, do you really want to move from `x`?

I really do want to move from x. I have thousands of lines doing that, though I expect a significant reduction when we start using coroutines.

My question is why do you want to move there instead of decay-copying?

Or do you want to do a decay-copy into `x`? Because the latter is what `std::thread/async` do. And we already have a proposal for shortening that.

This is wonderful. How about, in addition, a unary &&?   &&x == std::move(x).  std::move(x) is so common it deserves some syntax.

`std::move(x)` is sufficiently short that it doesn't need to be given special syntax. It's 9 characters. By contrast, "std::forward<decltype(x)>(x)` is 24 characters, not counting the size of the `x` variable.

Arthur O'Dwyer

unread,
Aug 16, 2017, 1:23:02 PM8/16/17
to ISO C++ Standard - Future Proposals
On Wed, Aug 16, 2017 at 8:08 AM, Nicol Bolas <jmck...@gmail.com> wrote:
> On Wednesday, August 16, 2017 at 4:47:32 AM UTC-4, Avi Kivity wrote:
>> On 08/15/2017 10:03 PM, Nicol Bolas wrote:
>> On Tuesday, August 15, 2017 at 2:29:27 PM UTC-4, Avi Kivity wrote:
>>>
>>> I would like to re-raise [&&x] as a shortcut for [x = std::move(x)] ()
>>> mutable.  It is seen extensively in continuation passing code, and since the
>>> language has first-class support for moves, lambdas should too.
>>
>> One could argue for the `&&x -> x = std::move(x)` part. But the automatic
>> `mutable`-izing part? No. Whether you agree with the `const`-by-default
>> nature of lambdas or not, we shouldn't create syntax that arbitrarily
>> changes the nature of a lambda.
>>
>> I see your point. How about, &&x translates to x = std::move(x) _and_ x is
>> a mutable member of the synthetic struct, even if `mutable` is not
>> specified?
>
> No. There's no reason to implicitly assume that, if a user wants to move an
> object into the lambda, they also mean to modify it. These are two
> orthogonal concepts.
>
> If you move a `unique_ptr` into a lambda, that doesn't mean you're going to
> modify the `unique_ptr` *itself*. You may modify what it points to, but
> `unique_ptr` doesn't forward `const` through to `get`.
>>
>> Also, do you really want to move from `x`?
>>
>> I really do want to move from x. I have thousands of lines doing that,
>> though I expect a significant reduction when we start using coroutines.
>
>
> My question is why do you want to move there instead of decay-copying?

My impression is that DECAY_COPY(x) always gives you a brand-new object (that is, it literally does move the representation of x from one address to another), whereas std::move(x) always gives you a reference (generally an rvalue reference) that usually doesn't require any extra codegen.

I strongly suspect that when Avi is concerned with "move-only and mutable" types, being captured by lambdas, potentially replaceable with coroutines, the specific types he's thinking about are std::promise and/or std::future. Using promises and futures with lambdas really does require a lot of noisy "mutable" keywords; his use-case is valid.

But I don't want to see "implicit mutable" in the language at this point, either. I'm certain that "mutable" should have been the default in 2011, but now that ship has been sailed for 6 years; we can't get it back.


>> This is wonderful. How about, in addition, a unary &&?   &&x ==
>> std::move(x).  std::move(x) is so common it deserves some syntax.
>
> `std::move(x)` is sufficiently short that it doesn't need to be given
> special syntax. It's 9 characters. By contrast,
> "std::forward<decltype(x)>(x)` is 24 characters, not counting the size of
> the `x` variable.

Today I learned that Louis Dionne has a highly plausible proposal for making "std::forward" lambda syntax consistent with the old non-lambda syntax.

    [](auto&& x) { return f(std::forward<decltype(x)>(x)); }

could after P0428 be written with named template parameters as

    []<class T>(T&& x) { return f(std::forward<T>(x)); }

Admittedly this is not as short as P0644's syntax

    [](auto&& x) { return f(>>x); }

or P0573's syntax

    x => f(>>x)

but I think it's more friendly to standardization than either of those, and at least the argument to std::forward wouldn't be gratuitously different from the usual one, anymore.

–Arthur

Nicol Bolas

unread,
Aug 16, 2017, 2:13:01 PM8/16/17
to ISO C++ Standard - Future Proposals


    [](auto&& x) { return f(std::forward<decltype(x)>(x)); }

could after P0428 be written with named template parameters as

    []<class T>(T&& x) { return f(std::forward<T>(x)); }

Admittedly this is not as short as P0644's syntax

    [](auto&& x) { return f(>>x); }

or P0573's syntax

    x => f(>>x)

but I think it's more friendly to standardization than either of those, and at least the argument to std::forward wouldn't be gratuitously different from the usual one, anymore.

The committee seemed at least receptive to the `>>` syntax for forwarding (according to this), which is why we got a separate paper for it. Don't assume failure before the battle begins.

Avi Kivity

unread,
Aug 16, 2017, 3:44:20 PM8/16/17
to std-pr...@isocpp.org, Nicol Bolas, arthur....@gmail.com
On 08/16/2017 06:08 PM, Nicol Bolas wrote:
On Wednesday, August 16, 2017 at 4:47:32 AM UTC-4, Avi Kivity wrote:
On 08/15/2017 10:03 PM, Nicol Bolas wrote:
On Tuesday, August 15, 2017 at 2:29:27 PM UTC-4, Avi Kivity wrote:
On 07/17/2017 09:46 PM, Arthur O'Dwyer wrote:
This paper proposes to remove two minor stumbling blocks to the teachability of lambda syntax — stumbling blocks of which many programmers may not even be aware.
The first is that [&x, =](){ return x; } is ill-formed.
The second is that [=, y](){ return x; } is ill-formed.
I propose to make both of these lambdas well-formed, with the obvious meanings.

http://quuxplusone.github.io/draft/liberal-syntax-for-lambdas.html

Comments welcome, either here or via email. Remember to read the paper first, though!


I would like to re-raise [&&x] as a shortcut for [x = std::move(x)] () mutable.  It is seen extensively in continuation passing code, and since the language has first-class support for moves, lambdas should too.

One could argue for the `&&x -> x = std::move(x)` part. But the automatic `mutable`-izing part? No. Whether you agree with the `const`-by-default nature of lambdas or not, we shouldn't create syntax that arbitrarily changes the nature of a lambda.

I see your point. How about, &&x translates to x = std::move(x) _and_ x is a mutable member of the synthetic struct, even if `mutable` is not specified?

No. There's no reason to implicitly assume that, if a user wants to move an object into the lambda, they also mean to modify it. These are two orthogonal concepts.

If you move a `unique_ptr` into a lambda, that doesn't mean you're going to modify the `unique_ptr` *itself*. You may modify what it points to, but `unique_ptr` doesn't forward `const` through to `get`.

It is, in fact, very typical when passing continuations. The object you moved into the lambda gets moved out into the next lambda when the continuation runs.

Making capture-by-value variables const makes sense, because modifications to them won't be reflected in the original variable, and so an easy to use mistake is prevented. But by moving the source object into the lambda you already declared you have no interest in it.


Also, do you really want to move from `x`?

I really do want to move from x. I have thousands of lines doing that, though I expect a significant reduction when we start using coroutines.

My question is why do you want to move there instead of decay-copying?


I have an object that is expensive to copy so I move it from continuation to contination.

future<foo> f(expensive x) {
    return foo1().then([x = std::move(x)] () mutable {
        return foo2().then([x = std::move(x)] () mutable {
              // do whatever with x
        });
    });

}


Or do you want to do a decay-copy into `x`? Because the latter is what `std::thread/async` do. And we already have a proposal for shortening that.

This is wonderful. How about, in addition, a unary &&?   &&x == std::move(x).  std::move(x) is so common it deserves some syntax.

`std::move(x)` is sufficiently short that it doesn't need to be given special syntax. It's 9 characters. By contrast, "std::forward<decltype(x)>(x)` is 24 characters, not counting the size of the `x` variable.

It's 9 characters for a very common operation. It's not uncommon to see calls to std::move() dominate an expression visually, while contributing little semantically. std::plus<>()(x, y) is also short, but it's not something you want to use when doing math. Nor would you want std::addressof() to take the address of a variable.




Avi Kivity

unread,
Aug 16, 2017, 3:51:15 PM8/16/17
to std-pr...@isocpp.org, Arthur O'Dwyer
Yes, future and promise (in the seastar namespace, not std, but same usage). Usually it's not the future/promise types that are moved, but state needed for continuations.


But I don't want to see "implicit mutable" in the language at this point, either. I'm certain that "mutable" should have been the default in 2011, but now that ship has been sailed for 6 years; we can't get it back.


Not even for capture-by-move variable? Not the entire lambda. It's likely that most lambdas that have a capture-by-move will also be mutable.



>> This is wonderful. How about, in addition, a unary &&?   &&x ==
>> std::move(x).  std::move(x) is so common it deserves some syntax.
>
> `std::move(x)` is sufficiently short that it doesn't need to be given
> special syntax. It's 9 characters. By contrast,
> "std::forward<decltype(x)>(x)` is 24 characters, not counting the size of
> the `x` variable.

Today I learned that Louis Dionne has a highly plausible proposal for making "std::forward" lambda syntax consistent with the old non-lambda syntax.

    [](auto&& x) { return f(std::forward<decltype(x)>(x)); }

could after P0428 be written with named template parameters as

    []<class T>(T&& x) { return f(std::forward<T>(x)); }

Admittedly this is not as short as P0644's syntax

    [](auto&& x) { return f(>>x); }

or P0573's syntax

    x => f(>>x)

but I think it's more friendly to standardization than either of those, and at least the argument to std::forward wouldn't be gratuitously different from the usual one, anymore.


I hope that being user-friendly to users also counts!

Nicol Bolas

unread,
Aug 16, 2017, 4:04:24 PM8/16/17
to ISO C++ Standard - Future Proposals, arthur....@gmail.com

I would like to see some evidence for that statement. You're talking about overturning a basic aspect of lambdas since they were standardized in 2011. It's not something that we should just do willy nilly.

I have a strong dislike for the idea that `[x = std::move(x)]` should in any way be different from `[&&x]`. There needs to be a really good reason for doing that.

Nicol Bolas

unread,
Aug 16, 2017, 4:34:11 PM8/16/17
to ISO C++ Standard - Future Proposals, jmck...@gmail.com, arthur....@gmail.com
On Wednesday, August 16, 2017 at 3:44:20 PM UTC-4, Avi Kivity wrote:
On 08/16/2017 06:08 PM, Nicol Bolas wrote:
On Wednesday, August 16, 2017 at 4:47:32 AM UTC-4, Avi Kivity wrote:
On 08/15/2017 10:03 PM, Nicol Bolas wrote:
Also, do you really want to move from `x`?

I really do want to move from x. I have thousands of lines doing that, though I expect a significant reduction when we start using coroutines.

My question is why do you want to move there instead of decay-copying?


I have an object that is expensive to copy so I move it from continuation to contination.

future<foo> f(expensive x) {
    return foo1().then([x = std::move(x)] () mutable {
        return foo2().then([x = std::move(x)] () mutable {
              // do whatever with x
        });
    });
}

Can you give me an example that wouldn't be far more readable if `co_await` got into C++20? I'm not a big fan of `co_await`, but it's basically going to make your "mutable move capture" feature obsolete.
Or do you want to do a decay-copy into `x`? Because the latter is what `std::thread/async` do. And we already have a proposal for shortening that.

This is wonderful. How about, in addition, a unary &&?   &&x == std::move(x).  std::move(x) is so common it deserves some syntax.

`std::move(x)` is sufficiently short that it doesn't need to be given special syntax. It's 9 characters. By contrast, "std::forward<decltype(x)>(x)` is 24 characters, not counting the size of the `x` variable.

It's 9 characters for a very common operation.

"very common operation" is based entirely on perspective. I don't do continuation-style programming, so what you call "very common" is very much not to me. Yes, a lot of people do this. But a lot of people don't. And this feature will affect all of them, by forcing them to learn that lambdas can sometimes have mutable captures without having the `mutable` keyword in them.

It's not uncommon to see calls to std::move() dominate an expression visually, while contributing little semantically.

"Dominate an expression visually"? Sure. But what you're talking about won't change that, since you're only talking about the case of lambda captures. Most expressions where `std::move` dominates the expression are variable declarations.

And I strongly disagree with the "contributing little semantically" suggestion. It's very important in many cases to know when an object has been moved from. Indeed, this was deliberately changed; earlier versions of rvalue references were very happy to move things without having to use `std::move`. That was changed because it make it too easy to break things by accident.

And broadly speaking, continuation lambdas tend to not be on the short side, so making their capture lists shorter isn't that important for readability. By contrast, lambdas that do forwarding do tend to be quite short. So making them shorter gives you more of an advantage.

Avi Kivity

unread,
Aug 16, 2017, 4:39:18 PM8/16/17
to std-pr...@isocpp.org, Nicol Bolas, arthur....@gmail.com
Here's one sample:

$ git grep -E '\[.*=.*std::move.*\]' | wc -l
319
$ git grep -E '\[.*=.*std::move.*\].*mutable' | wc -l
142

So, about half. The ones that don't have mutable are either the last continuation in the chain (so don't need to be moved out), or just performance bugs - the move gets converted into a copy.



I have a strong dislike for the idea that `[x = std::move(x)]` should in any way be different from `[&&x]`. There needs to be a really good reason for doing that.

The reason is, by making the lambda the owner of the contents, you're no longer protecting the original object from missing a mutation.

By forcing the user to make the entire lambda mutable, you're reducing the ability to detect this kind of bug on variables that _are_ copied by value.

Avi Kivity

unread,
Aug 16, 2017, 4:49:39 PM8/16/17
to std-pr...@isocpp.org, Nicol Bolas, arthur....@gmail.com
On 08/16/2017 11:34 PM, Nicol Bolas wrote:
On Wednesday, August 16, 2017 at 3:44:20 PM UTC-4, Avi Kivity wrote:
On 08/16/2017 06:08 PM, Nicol Bolas wrote:
On Wednesday, August 16, 2017 at 4:47:32 AM UTC-4, Avi Kivity wrote:
On 08/15/2017 10:03 PM, Nicol Bolas wrote:
Also, do you really want to move from `x`?

I really do want to move from x. I have thousands of lines doing that, though I expect a significant reduction when we start using coroutines.

My question is why do you want to move there instead of decay-copying?


I have an object that is expensive to copy so I move it from continuation to contination.

future<foo> f(expensive x) {
    return foo1().then([x = std::move(x)] () mutable {
        return foo2().then([x = std::move(x)] () mutable {
              // do whatever with x
        });
    });
}

Can you give me an example that wouldn't be far more readable if `co_await` got into C++20? I'm not a big fan of `co_await`, but it's basically going to make your "mutable move capture" feature obsolete.

I don't yet have experience with coroutines. I do hope they make this usage of lambdas obsolete.

I plan to start making use of clang coroutines soon, even before standardization, because of the great promise they show. But I still think move-capture is useful,
but as you would say this is based less on evidence and more on feeling.


Or do you want to do a decay-copy into `x`? Because the latter is what `std::thread/async` do. And we already have a proposal for shortening that.

This is wonderful. How about, in addition, a unary &&?   &&x == std::move(x).  std::move(x) is so common it deserves some syntax.

`std::move(x)` is sufficiently short that it doesn't need to be given special syntax. It's 9 characters. By contrast, "std::forward<decltype(x)>(x)` is 24 characters, not counting the size of the `x` variable.

It's 9 characters for a very common operation.

"very common operation" is based entirely on perspective. I don't do continuation-style programming, so what you call "very common" is very much not to me. Yes, a lot of people do this. But a lot of people don't. And this feature will affect all of them, by forcing them to learn that lambdas can sometimes have mutable captures without having the `mutable` keyword in them.


If C++ was aiming to be a small, elegant, consistent language, then I think it failed. At least it can try to be concise.


It's not uncommon to see calls to std::move() dominate an expression visually, while contributing little semantically.

"Dominate an expression visually"? Sure. But what you're talking about won't change that, since you're only talking about the case of lambda captures. Most expressions where `std::move` dominates the expression are variable declarations.

No, above I also proposed a unary && operator (inspired by unary >>). And moves are everywhere, in function calls particularly.



And I strongly disagree with the "contributing little semantically" suggestion. It's very important in many cases to know when an object has been moved from. Indeed, this was deliberately changed; earlier versions of rvalue references were very happy to move things without having to use `std::move`. That was changed because it make it too easy to break things by accident.


Of course it's important to know it, but it's less important than, say, if you're passing x or y. Having the reference type dominate over the variable name is not helping readability.

    f(by_ref, &by_addr, &&moved)

vs

    f(std::ref(by_ref), std::addressof(by_addr), std::move(moved))

the second is noisy and harder to read, IMO.



And broadly speaking, continuation lambdas tend to not be on the short side, so making their capture lists shorter isn't that important for readability. By contrast, lambdas that do forwarding do tend to be quite short. So making them shorter gives you more of an advantage.


I've seen a lot more move-capture lambdas than forwarding lambdas; but of course that's just my use case. Forwarding is limited to generic (library) code which is usually dominated by application code.

std::plus<>()(x, y) is also short, but it's not something you want to use when doing math. Nor would you want std::addressof() to take the address of a variable.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/ed2126d1-debe-43fe-b8bc-35c1a884cc5e%40isocpp.org.


Nicol Bolas

unread,
Aug 16, 2017, 5:00:09 PM8/16/17
to ISO C++ Standard - Future Proposals, jmck...@gmail.com, arthur....@gmail.com

You made a very general statement: "It's likely that most lambdas that have a capture-by-move will also be mutable." That statement did not limit itself to code you're responsible for. So do you have evidence that this is true of C++ code in general?

I have a strong dislike for the idea that `[x = std::move(x)]` should in any way be different from `[&&x]`. There needs to be a really good reason for doing that.

The reason is, by making the lambda the owner of the contents, you're no longer protecting the original object from missing a mutation.

But both of them are making the lambda "the owner of the contents". My question is why should one way of transferring ownership be different from the other?

By forcing the user to make the entire lambda mutable, you're reducing the ability to detect this kind of bug on variables that _are_ copied by value.

I don't know what you mean by that. Your original declaration was that the use of `&&x` in a capture list causes the lambda to become `mutable`:


> I would like to re-raise [&&x] as a shortcut for [x = std::move(x)] () mutable.

Are you now saying that you want it to be limited to just `x` being `mutable`? Wouldn't it make more sense to just allow you to do this `[mutable &&x]`, `[mutable x = std::move(x)]` and so forth?

Why should mutability be tied to how the capture variable was initialized? These are entirely orthogonal constructs, so they should be handled with orthogonal syntax.

Avi Kivity

unread,
Aug 17, 2017, 3:16:46 AM8/17/17
to std-pr...@isocpp.org, Nicol Bolas, arthur....@gmail.com
Unfortunately, I do not.

The style of code used in my example, while not very common, is also not specific to my application. So I expect my code would not be the only beneficiary.



I have a strong dislike for the idea that `[x = std::move(x)]` should in any way be different from `[&&x]`. There needs to be a really good reason for doing that.

The reason is, by making the lambda the owner of the contents, you're no longer protecting the original object from missing a mutation.

But both of them are making the lambda "the owner of the contents". My question is why should one way of transferring ownership be different from the other?

Because one way is set in stone and cannot be modified. The other way can still be changed, and it should be chosen to fit the user's needs, not an overriding sense of consistency. Consistency is good, but it's not something that C++ will be good at. I'm not proposing that we should ignore it, just that it should not be the top priority.



By forcing the user to make the entire lambda mutable, you're reducing the ability to detect this kind of bug on variables that _are_ copied by value.

I don't know what you mean by that. Your original declaration was that the use of `&&x` in a capture list causes the lambda to become `mutable`:

> I would like to re-raise [&&x] as a shortcut for [x = std::move(x)] () mutable.

Are you now saying that you want it to be limited to just `x` being `mutable`? Wouldn't it make more sense to just allow you to do this `[mutable &&x]`, `[mutable x = std::move(x)]` and so forth?

I changed my position a few emails ago, you must have missed it.

I do like [mutable x], but it will be rare. [mutable &&x] on the other hand will be very common, and on many occasions forgetting it will be a subtle mistake (for copy/move types) or a compile error (for move-only types) so to remove this class of error it should be the default.



Why should mutability be tied to how the capture variable was initialized? These are entirely orthogonal constructs, so they should be handled with orthogonal syntax.


Because, for copy capture, const is the sensible bug-preventing default, while for move-capture, mutable is the sensible bug-preventing default.

Ryan Nicholl

unread,
Aug 23, 2017, 7:29:20 PM8/23/17
to ISO C++ Standard - Future Proposals, jmck...@gmail.com, arthur....@gmail.com
I think we should prefer consistency of the lambda behavior being const. A move construction that is const is not always wrong. If the value does not need to be modified later but needs to be preserved because the lifetime of the input is ending then &&x makes sense.
However, [mutable &&x] I think is better than the alternative of [const &&x], lambdas are implicitly const. Leaving that alone is probably best. Making the const-ness of values depend on the way they are initialized doesn't make sense, no matter the common use cases.
Reply all
Reply to author
Forward
0 new messages