Statement expressions proposal

586 views
Skip to first unread message

szollos...@gmail.com

unread,
Feb 6, 2017, 8:57:56 PM2/6/17
to ISO C++ Standard - Future Proposals
Hi,

Please find below the initial draft for a statement expression proposal. Any ideas, hints, suggestions are welcome. I understand that it's my first proposal and it might take several iterations if ever to make if fly, so I'd be very glad about points where I could make it better (one of them is formatting, which I'm going to fix); whether to add more detail or less; whether to change examples.

Link to the proposal's initial draft:
http://lorro.hu/cplusplus/statement_expressions.html

Thanks in advance,
-lorro

gmis...@gmail.com

unread,
Feb 6, 2017, 9:39:18 PM2/6/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
I haven't yet fully grasped this proposal yet, but one thing that immediately stands out as undesirable to me is the sort of floating expression
at the end. e.g. this from your example:

int product = ({
    int j = 1;
    for (auto i : v)
        j *= i;
    j;
});

I very much would prefer at a minimum, this:

int product = ({
    int j = 1;
    for (auto i : v)
        j *= i;
    produce j;
});

More generally I wonder if extending inline functions with something might be a nicer route:

inline int getProductOrReturn() // or maybe inline statement keyword some introduction is needed.
{
    if (somethingOutlandish())
        inline return; // No really return from wherever this code gets inlined.
    int j = 1;
    for (auto i : v)
        j *= i;
    return j;
};

But I haven't thought too hard about this later idea. What you are proposing may be preferable to that, I haven't digested your proposal yet.

David Krauss

unread,
Feb 6, 2017, 10:02:04 PM2/6/17
to std-pr...@isocpp.org
There was a recent discussion on this list, “capturing monads in lambda,” about continuation capture in lambda expressions, i.e. [return]{ return 23; }.

That proposal suggested that the continuation could be passed into, for instance, functions from <algorithm> and it would implement dynamic unwinding like throw. You seem to be not proposing that, but it’s unclear what mechanism prohibits it from happening. What happens when a reference or pointer is taken to a named statement expression? Is the name a new kind of entity?

The entire SE has the evaluation type of the last expression, but special care must be taken as control might be transferred before reaching the last expression.

A better solution is needed. GCC statement expressions are a precedent and they’re widely implemented, but usability is a more important factor.

Tony V E

unread,
Feb 6, 2017, 10:46:06 PM2/6/17
to ISO C++ Standard - Future Proposals
I highly recommend, for almost any proposal, to include "before and after tables" (I think the committee has a some nick name for these), showing what you would currently need to write using C++17 on the left, and how it could be rewritten in C++20 using your proposal on the right.

The table thus shows both motivation and syntax in a compact form. 

Sent from my BlackBerry portable Babbage Device
Sent: Monday, February 6, 2017 8:57 PM
To: ISO C++ Standard - Future Proposals
Subject: [std-proposals] Statement expressions proposal

--
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/1df2e5b0-bbf8-4f5f-9eff-5e339516b99e%40isocpp.org.

Viacheslav Usov

unread,
Feb 7, 2017, 5:19:00 AM2/7/17
to ISO C++ Standard - Future Proposals
I may be misunderstanding something, but how is the proposed ({ statements; expr; }) different from [&]{ statements; return expr; }()? Is there more to it?

The parametric part looks even more like a regular lambda definition. What is the benefit of all this?

Cheers,
V.

TONGARI J

unread,
Feb 7, 2017, 9:20:43 AM2/7/17
to std-pr...@isocpp.org
2017-02-07 18:18 GMT+08:00 Viacheslav Usov <via....@gmail.com>:
I may be misunderstanding something, but how is the proposed ({ statements; expr; }) different from [&]{ statements; return expr; }()? Is there more to it?

The control flow. You can use 'return', 'break', etc inside the block that can get you to the outer block, which is what lambda expression can't do.

Nicol Bolas

unread,
Feb 7, 2017, 9:50:08 AM2/7/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com, gmis...@gmail.com
On Monday, February 6, 2017 at 9:39:18 PM UTC-5, gmis...@gmail.com wrote:
I haven't yet fully grasped this proposal yet, but one thing that immediately stands out as undesirable to me is the sort of floating expression
at the end. e.g. this from your example:

int product = ({
    int j = 1;
    for (auto i : v)
        j *= i;
    j;
});

I very much would prefer at a minimum, this:

int product = ({
    int j = 1;
    for (auto i : v)
        j *= i;
    produce j;
});

This would also be important if you have any significant control flow going on. Like `if(...) produce j; else produce k;` Obviously, you could ?: that, but there are more complex situations where that becomes impractical.

To bikeshed for a moment, we already have `co_return` in P0057. So different forms of `return` statements are now at least semi-standard. Having a new form of "return" that returns a value as the expression of the innermost expression statement would be valid. Perhaps `in_return`.

More generally I wonder if extending inline functions with something might be a nicer route:

The point of a statement expression is that it's part of the current function. It's not doing a separate call; it's just a block of code that produces a value. That way, you can still do things like access local variables and parameters, use `break` and `return` as if you were in the function (which you are) etc.

Calling a function, even an `inline` function, is not helpful for this purpose.

Ville Voutilainen

unread,
Feb 7, 2017, 9:52:36 AM2/7/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com, G M
On 7 February 2017 at 16:50, Nicol Bolas <jmck...@gmail.com> wrote:
>> More generally I wonder if extending inline functions with something might
>> be a nicer route:
>
>
> The point of a statement expression is that it's part of the current
> function. It's not doing a separate call; it's just a block of code that
> produces a value. That way, you can still do things like access local
> variables and parameters, use `break` and `return` as if you were in the
> function (which you are) etc.


Here's a question: how do I get from this proposal to a
statement-expression template?
I want to write generic code blocks that I can glue into other places,
passing template
arguments that guide how the code is instantiated.

Viacheslav Usov

unread,
Feb 7, 2017, 9:57:02 AM2/7/17
to ISO C++ Standard - Future Proposals
On Tue, Feb 7, 2017 at 3:20 PM, TONGARI J <tong...@gmail.com> wrote:

> The control flow. You can use 'return', 'break', etc inside the block that can get you to the outer block, which is what lambda expression can't do.

I see.

Well, not really. I do not see that explained in the proposal at all. I only see a return in an if statement there, but that you can have in a lambda.

I'd say that the proposal has a lot to explain before it can be called a proposal.

Cheers,
V.

Barry Revzin

unread,
Feb 7, 2017, 9:57:28 AM2/7/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
One thing we would want to ensure for something like this example:

#define return_if_false(a) ({ auto tmp = (a); if (!tmp) return std::move(tmp); std::move(tmp); })

is that all the return copy-elision rules apply in this case as well. We'd probably want to be able to write it like:

#define return_if_false(a) ({ auto tmp = (a); if (!tmp) return tmp; tmp; })
auto x = return_if_false(y); // tmp is constructed in place in x, if the return branch isn't taken

GCC's current implementation doesn't have this kind of copy-elision. 

Nicol Bolas

unread,
Feb 7, 2017, 10:04:28 AM2/7/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com

I see a lot of what I would consider needless restrictions in this proposal. It makes no sense to only allow some statements inside a statement expression. Functions don't have restrictions on the classes you can create within them, besides the fact that they cannot be templates. As such, there's no reason for statement expressions to have that restriction either.

Either a statement expression allows statements, or it doesn't. It shouldn't be a subset of statements.

It's good that you've looked at existing implementations. But you shouldn't let that restrict the proposal unduly. Not unless you have specific knowledge that such implementations would be a hardship.

At the end of the day, a "statement expression" isn't really a thing. It's just a grammatical construct that allows multiple statements to potentially resolve to a value that gets used to initialize an object. It should not be limited in what it can do relative to that.

Now, parameters and "named statements", I don't think those should even be part of the proposal. That's macro-style thinking, and that's not really what this is about. Once it has a name, that means you have to decide what that name means. Is it a function? If so, what can you do with it. And so on. Those are too complicated to deal with in such a proposal.

Nicol Bolas

unread,
Feb 7, 2017, 10:05:53 AM2/7/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com, gmis...@gmail.com

You write a macro.

Nicol Bolas

unread,
Feb 7, 2017, 10:07:01 AM2/7/17
to ISO C++ Standard - Future Proposals


On Tuesday, February 7, 2017 at 9:57:02 AM UTC-5, Viacheslav Usov wrote:
On Tue, Feb 7, 2017 at 3:20 PM, TONGARI J <tong...@gmail.com> wrote:

> The control flow. You can use 'return', 'break', etc inside the block that can get you to the outer block, which is what lambda expression can't do.

I see.

Well, not really. I do not see that explained in the proposal at all. I only see a return in an if statement there, but that you can have in a lambda.

No, you can't. The return in the statement expression returns from the function itself. A return in a lambda returns from the lambda.
 

Nicol Bolas

unread,
Feb 7, 2017, 10:13:07 AM2/7/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com

Presumably, elision would happen naturally. `tmp` is a local variable. `return tmp` is a return statement. It doesn't matter where this statement is, NRVO can be applied if it is returning a local (non-parameter) object that matches the type.

Similarly, you seem to be talking more about lvalue-to-xvalue conversion than NRVO. That is, `return tmp;`, even if the compiler cannot elide the named variable, will still convert the lvalue into an xvalue for the purpose of initializing the return value object. So `return tmp;` will move into the return value if it is able.

And this suggests even more strongly the need for an explicit syntax to resolve the value of a statement expression. That way, we can hook `in_return tmp;`  into the lvalue-to-xvalue machinery that already exists for `return` and `co_return`. This would also allow us a way to specify elision rules for `in_return`'s values in statement expressions.

Viacheslav Usov

unread,
Feb 7, 2017, 10:13:22 AM2/7/17
to ISO C++ Standard - Future Proposals
On Tue, Feb 7, 2017 at 4:07 PM, Nicol Bolas <jmck...@gmail.com> wrote:

> No, you can't. The return in the statement expression returns from the function itself. A return in a lambda returns from the lambda.

Per your unpublished proposal, perhaps.

Per the subject of this thread, who knows.

Cheers,
V.

Nicol Bolas

unread,
Feb 7, 2017, 10:29:16 AM2/7/17
to ISO C++ Standard - Future Proposals

... we do know. It's right there in the Motivation section. The proposal even has a pointless feature macro for it: __cplusplus_se_ret.

Viacheslav Usov

unread,
Feb 7, 2017, 10:38:46 AM2/7/17
to ISO C++ Standard - Future Proposals
On Tue, Feb 7, 2017 at 4:29 PM, Nicol Bolas <jmck...@gmail.com> wrote:

> ... we do know. It's right there in the Motivation section.

Nope. The motivation section has no verbiage like from the function itself. The sample that it has can be interpreted either way (and it is silly either way). You may have experience with something named "statement expressions" in some other language or on some particular implementation, so what they want to say may be (you think) clear to you. It is not to me. To me that looks like a very low quality proposal, whose readers need to guess what is being proposed.

Cheers,
V.

Matthew Woehlke

unread,
Feb 7, 2017, 10:48:07 AM2/7/17
to std-pr...@isocpp.org
On 2017-02-06 21:39, gmis...@gmail.com wrote:
> I very much would prefer at a minimum, this:
>
> int product = ({
> int j = 1;
> for (auto i : v)
> j *= i;
> produce j;
> });

Bikeshed: `produce` or `yield`? Or, if abbreviated lambdas is accepted,
unary prefix `=>`?

I'd really like to converge to only *one* new keyword that can be used
in multiple contexts. Besides this and P0057, think generator functions,
also.

Moreover, if we do something like this, can you write:

auto x = ({
if (expr)
produce y; // SE execution ends here if we get here
// additional logic
produce z;
});

...?

--
Matthew

Nicol Bolas

unread,
Feb 7, 2017, 10:49:37 AM2/7/17
to ISO C++ Standard - Future Proposals
On Tuesday, February 7, 2017 at 10:38:46 AM UTC-5, Viacheslav Usov wrote:
On Tue, Feb 7, 2017 at 4:29 PM, Nicol Bolas <jmck...@gmail.com> wrote:

> ... we do know. It's right there in the Motivation section.

Nope. The motivation section has no verbiage like from the function itself. The sample that it has can be interpreted either way (and it is silly either way).

How exactly is it "silly either way"? If `return tmp` returns the value from the function, then it's clear what's happening. It will initialize the variable or return from the function.

It may be unnecessary for that particular case, but it is hardly silly.
 
You may have experience with something named "statement expressions" in some other language or on some particular implementation, so what they want to say may be (you think) clear to you. It is not to me. To me that looks like a very low quality proposal, whose readers need to guess what is being proposed.

I agree that the proposal should actually explain what a statement expression is, conceptually.

I would also suggest that motivations besides "wrap it in a macro" be specified. The current motivation and examples all suggest that this feature exists solely for the benefit of macro programming.

And that's not something we should invent language features for.

Matthew Woehlke

unread,
Feb 7, 2017, 10:57:51 AM2/7/17
to std-pr...@isocpp.org
On 2017-02-06 22:01, David Krauss wrote:
> There was a recent discussion on this list, “capturing monads in
> lambda,” about continuation capture in lambda expressions, i.e.
> [return]{ return 23; }.
>
> That proposal suggested that the continuation could be passed into,
> for instance, functions from <algorithm> and it would implement dynamic
> unwinding like throw. You seem to be not proposing that, but it’s
> unclear what mechanism prohibits it from happening. What happens when a
> reference or pointer is taken to a named statement expression?

One possible answer is that they are closer to macros/templates (i.e.
the compiler knows their definition and substitutes them inline at point
of use), and you cannot take their address.

This part of the proposal might warrant moving to a "Future Directions"
section.

--
Matthew

Ville Voutilainen

unread,
Feb 7, 2017, 11:52:06 AM2/7/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com, G M
Ah, good to know you're so opposed to this proposal that you want to
kill it right away, before it goes any further.

Nicol Bolas

unread,
Feb 7, 2017, 1:44:06 PM2/7/17
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com
On Tuesday, February 7, 2017 at 10:48:07 AM UTC-5, Matthew Woehlke wrote:
On 2017-02-06 21:39, gmis...@gmail.com wrote:
> I very much would prefer at a minimum, this:
>
> int product = ({
>     int j = 1;
>     for (auto i : v)
>         j *= i;
>     produce j;
> });

Bikeshed: `produce` or `yield`? Or, if abbreviated lambdas is accepted,
unary prefix `=>`?

I'd really like to converge to only *one* new keyword that can be used
in multiple contexts. Besides this and P0057, think generator functions,
also.

None of the P0057 ones would be appropriate. `co_yield` and `co_return` should be things that you can do within statement expressions, which cause the function as a whole to co_yield or co_return. You ought to be able to use them in statement expressions of coroutines.

That's why it needs to be a new keyword.
 
Moreover, if we do something like this, can you write:

  auto x = ({
    if (expr)
      produce y; // SE execution ends here if we get here
    // additional logic
    produce z;
  });

...?

It would resolve exactly like return type deduction: if the statement expression's `in_return` or whatever do not all deduce to the same type, you get UB.

Though the exact form of the deduction (`auto` vs `decltype(auto)`) probably should be ironed out.

Nicol Bolas

unread,
Feb 7, 2017, 1:46:55 PM2/7/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com, gmis...@gmail.com

I don't want C++ to have named/parameterized statement expressions at all, let alone templates of them. I see utility for statement expressions as a concept beyond "way to stick stuff into macros that don't interfere with other code", and I don't think the feature should be presented as a way to that end.

Matthew Woehlke

unread,
Feb 7, 2017, 2:30:39 PM2/7/17
to std-pr...@isocpp.org
On 2017-02-07 13:44, Nicol Bolas wrote:
> On Tuesday, February 7, 2017 at 10:48:07 AM UTC-5, Matthew Woehlke wrote:
>> On 2017-02-06 21:39, gmis...@gmail.com <javascript:> wrote:
>>> I very much would prefer at a minimum, this:
>>>
>>> int product = ({
>>> int j = 1;
>>> for (auto i : v)
>>> j *= i;
>>> produce j;
>>> });
>>
>> Bikeshed: `produce` or `yield`? Or, if abbreviated lambdas is accepted,
>> unary prefix `=>`?
>>
>> I'd really like to converge to only *one* new keyword that can be used
>> in multiple contexts. Besides this and P0057, think generator functions,
>> also.
>
> None of the P0057 ones would be appropriate. `co_yield` and `co_return`
> should be things that you can do *within* statement expressions, which
> cause the function as a whole to co_yield or co_return. You ought to be
> able to use them in statement expressions of coroutines.
>
> That's why it needs to be a new keyword.

Arf. You're right, and the same goes for generators or whatever; the SE
should be able to use those control keywords and have them mean what
they mean outside the SE. So, yeah, you need a SE-specific keyword. (In
which case, if abbreviated lambdas are accepted, I'm going to vote for
`=>` :-).)

>> Moreover, if we do something like this, can you write:
>>
>> auto x = ({
>> if (expr)
>> produce y; // SE execution ends here if we get here
>> // additional logic
>> produce z;
>> });
>>
>> ...?
>
> It would resolve exactly like return type deduction: if the statement
> expression's `in_return` or whatever do not all deduce to the same type,
> you get UB.

Uh... UB? Really? I would think ill-formed, which is how return type
deduction works...

Actually, I hadn't even thought about that, though the answer ("like
return type deduction") is obvious. I find it more odd that you can
cause the SE to early-exit *at all* (besides something that tosses flow
even further, like `break` or `return`). I guess you're okay with that,
given you're thinking past it.

--
Matthew

Nicol Bolas

unread,
Feb 7, 2017, 3:01:26 PM2/7/17
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com

That would be confusing. After all, `=>` doesn't mean `return`; it means a lot more than just to return something. It also specifically overrides the default return type deduction on lambdas (using `decltype(auto)` instead of `auto`). And it includes `noexcept` stuff.

More to the point, `=>` is for defining a function, and SEs explicitly are not functions. Using `=>` to mean "SE resolves to X" would be very odd indeed.

>> Moreover, if we do something like this, can you write:
>>
>>   auto x = ({
>>     if (expr)
>>       produce y; // SE execution ends here if we get here
>>     // additional logic
>>     produce z;
>>   });
>>
>> ...?
>
> It would resolve exactly like return type deduction: if the statement
> expression's `in_return` or whatever do not all deduce to the same type,
> you get UB.

Uh... UB? Really? I would think ill-formed, which is how return type
deduction works...

Sorry, brain fart. Yes, ill-formed.
 
Actually, I hadn't even thought about that, though the answer ("like
return type deduction") is obvious. I find it more odd that you can
cause the SE to early-exit *at all* (besides something that tosses flow
even further, like `break` or `return`). I guess you're okay with that,
given you're thinking past it.

The way I see it, once you want to be able to have statements inside of expressions, you shouldn't add restrictions to what kind of statements you want in there unless it's absolutely necessary. So having the expression evaluate to a value at an arbitrary location is perfectly understandable.

The main thing I want to make sure we don't do is turn these into "functors that are magically connected to the location of their creation". They should be evaluated in exactly and only one place: the place where they are defined.

Vicente J. Botet Escriba

unread,
Feb 7, 2017, 4:20:09 PM2/7/17
to std-pr...@isocpp.org, szollos...@gmail.com, G M
Wouldn't the section "Support for named parametric SE" respond to this need?


Vicente

Thiago Macieira

unread,
Feb 7, 2017, 6:11:25 PM2/7/17
to std-pr...@isocpp.org
On terça-feira, 7 de fevereiro de 2017 22:20:41 PST TONGARI J wrote:
> The control flow. You can use 'return', 'break', etc inside the block that
> can get you to the outer block, which is what lambda expression can't do.

while ({(break; true;)})
continue;

Is that break in scope of the while?

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center

TONGARI J

unread,
Feb 7, 2017, 8:45:25 PM2/7/17
to std-pr...@isocpp.org
2017-02-08 7:11 GMT+08:00 Thiago Macieira <thi...@macieira.org>:
On terça-feira, 7 de fevereiro de 2017 22:20:41 PST TONGARI J wrote:
> The control flow. You can use 'return', 'break', etc inside the block that
> can get you to the outer block, which is what lambda expression can't do.

while ({(break; true;)})
        continue;

Is that break in scope of the while?

Note that you get the syntax the other way round, it's ({}) not {()}.
It's an interesting question, seems GCC and Clang disagree here.
My gut feeling is that the 'break' should apply to some outer level other than the while-stmt, as what GCC does.

Thiago Macieira

unread,
Feb 7, 2017, 11:33:47 PM2/7/17
to std-pr...@isocpp.org
On quarta-feira, 8 de fevereiro de 2017 09:45:23 PST TONGARI J wrote:
> Note that you get the syntax the other way round, it's ({}) not {()}.

Sorry, just trying to write from memory.

> It's an interesting question, seems GCC and Clang disagree here.
> My gut feeling is that the 'break' should apply to some outer level other
> than the while-stmt, as what GCC does.

Another question (just for food for thought): would statement expressions be
allowed in constexpr contexts?

struct S
{
int i;
constexpr S() : i(({ something_constexpr(); 1; })) {}
}

My gut feeling is that so long it is still constexpr, it should be allowed.

Richard Smith

unread,
Feb 8, 2017, 7:30:56 PM2/8/17
to std-pr...@isocpp.org
Actually, that's not what GCC does. Its diagnostics and code generation are out of sync: it requires a surrounding loop construct but the break statement actually breaks out of the inner while statement.

szollos...@gmail.com

unread,
Feb 8, 2017, 8:11:46 PM2/8/17
to ISO C++ Standard - Future Proposals
Hi,

Thanks for all the valuable responses. I'll try to answer the open items and those where I can add.

gmis> produce j;
Nicol Bolas> =>
I'm okay with either version. Note that there are two distinct changes here:
  • Having a keyword/macro before the last expression. `=>' would be intuitive if simple lambda expressions are accepted. `produce' means it can be defined as empty in current compilers to make them conforming.
  • Allowing multiple `=>'/`produce' points. This is useful (and should be included), but note that this means we're trading the return-from-caller problem to produce-from caller problem.
David Krauss> “capturing monads in lambda,” about continuation capture in lambda expressions, i.e. [return]{ return 23; }
Ville Voutilainen> I want to write generic code blocks that I can glue into other places,
passing template arguments that guide how the code is instantiated.

Yep, that's from me as well. I'm working on it, with many open items as complexity is blowing up exponentially. Currently I think capture-return should be a part of the template argument list, without being an actual template argument; it should rather be auto-captured there. Once that's ready for proposal, this can be thought of as a simplified case. In case you're interested, these three should be equal:

template<return>
int fn() { return 23; }

// note that neither of the functions have a direct return statement
int test_fn() { fn(); }
int test_se() { ({ return 23; }); }
int test_l()  { []<return>(){ return 23; }(); }

Tony V E> before and after tables
Wish I could! Currently I cannot produce the same functionality without major redesign of the caller (or modification to the caller and abusing exceptions badly). Or do you mean to include caller as well?

Viacheslav Usov> I do not see that explained in the proposal at all.
Good point. Added a short one on this in the introduction.

Barry Revzin> copy-elision
I've added it as an optional (hopefully implemented feature).

... to be continued in next mail ...

Thanks, -lorro

szollos...@gmail.com

unread,
Feb 8, 2017, 9:04:04 PM2/8/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
... continued from previous mail ...

Nicol Bolas> But you shouldn't let that restrict the proposal unduly. Not unless you have specific knowledge that such implementations would be a hardship.
I'd be the best if all the features were accepted by the committee; however, I do not want to keep it from being standardized if some points are inconclusive/rejected. Therefore I suggested a 'tristate ticklist' where each feature can be accepted, rejected (in which 2 cases feature disc. macros are unnecessary) or optionally accepted (in which case a feat. disc. macro is suggested). Optional, as in other parts of the standard means: you, as a compiler writer 'don't have to do this, but if you do, do it this way'.
Note that I'm new and not aware of the voting process. If such a proposal cannot be made, let me know and I come up with something else.
As for hardships, I've checked some manuals. Intel especially claims that destructors and some other features are not allowed due to the difficulty of them (think of it this way: when should an object created in SE be destructed? If at produce, we can't have RAII; if at the end of the statement, we need to specifically state that SE body is function-like in the sense that "temporaries don't last till the end of the statement").

Nicol Bolas> Now, parameters and "named statements", I don't think those should even be part of the proposal. I don't want C++ to have named/parameterized statement expressions at all, let alone templates of them.
I understand you don't want these. I'd like you to understand that many, including myself, do. Thus it - sooner or later - gets implemented (read: I'm already looking into how to do it on Itanium ABI); and if it's not standardized, it gets implemented multiple ways by multiple vendors. That's a nightmarish vision we want to avoid. Hence I proposed these as optional features. I do not ask you to 'vote for accepting it', I ask you to consider these under the 'don't have to do this, but if you do, do it this way'-rule.
Note that, by having them as inline templates (or similar), they are still 'macro-like', but with the benefit of type safety, ADL, namespaces and no `#undef' and redefining (which I actually consider a benefit). If the issue you see is with some features of functions/templates/lambdas that macros don't have or do much differently, please help me understand and I'll try to come up with an idea that solves both these issues and the above-mentioned benefits.
Viacheslav Usov> The motivation section has no verbiage like [return] from the function itself.
I've added an explicit description of this in the introduction, that is, `return' inside a SE means return from the evaluating point of the statement expression; similarly for `break', `continue', et al. Indeed, it was not clear if you don't assume current SE from gcc.
NB. another possible keyword for `produce' is `continue'. It has the advantage of not needing a new keyword and that it's similar in meaning to the current, loop-based `continue'. One can distinguish `continue k' from `continue', so we can access the loop-based one as long as the function is not void. If it is, we might need another syntax, perhaps `continue []{}()' (where the lambda is any no-op expr. evaluating to void).

Nicol Bolas> I would also suggest that motivations besides "wrap it in a macro" be specified. The current motivation and examples all suggest that this feature exists solely for the benefit of macro programming.
If you could elaborate a bit on this, I'd be happy to fix these parts. For me, the non-macro-like use case would be the parametric one, but I feel you're thinking of something else.

David Krauss> What happens when a reference or pointer is taken to a named statement expression? Is the name a new kind of entity?
I'd suggest allowing for both as long as used within the function. In runtime, this is not an issue; in compile-time, this has to be validated by the compiler. Note that a named SE is like a template in which `return' is resolved by the function instantiating it.

Thiago Macieira, TONGARI J, Richard Smith> while ({(break; true;)})
Good question! My gut feeling tells to follow what gcc currently compiles, i.e., the control-flow keywords should be overridden as soon as the keyword is 'in scope', similarly to a variable being considered 'in scope' as soon as brought in scope. That said, I'm okay to change this if you prefer otherwise.

Thanks again for all the comments and help on this,
-lorro

Nicol Bolas

unread,
Feb 8, 2017, 9:56:59 PM2/8/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
On Wednesday, February 8, 2017 at 9:04:04 PM UTC-5, szollos...@gmail.com wrote:
... continued from previous mail ...

Nicol Bolas> But you shouldn't let that restrict the proposal unduly. Not unless you have specific knowledge that such implementations would be a hardship.
I'd be the best if all the features were accepted by the committee; however, I do not want to keep it from being standardized if some points are inconclusive/rejected. Therefore I suggested a 'tristate ticklist' where each feature can be accepted, rejected (in which 2 cases feature disc. macros are unnecessary) or optionally accepted (in which case a feat. disc. macro is suggested). Optional, as in other parts of the standard means: you, as a compiler writer 'don't have to do this, but if you do, do it this way'.
Note that I'm new and not aware of the voting process. If such a proposal cannot be made, let me know and I come up with something else.
As for hardships, I've checked some manuals. Intel especially claims that destructors and some other features are not allowed due to the difficulty of them

 
(think of it this way: when should an object created in SE be destructed? If at produce, we can't have RAII;

Um, why can't you have RAII? That's like saying that you can't have RAII if you use a lambda to mimic expression statements.

Expression statements should be scopes; hence the use of {} to declare them. Just like any scope, automatic variables end their lifetimes when control leaves that scope, whether through "produce" (ugh), return, break, or whatever. I see no reason why this would affect RAII.

Nicol Bolas> Now, parameters and "named statements", I don't think those should even be part of the proposal. I don't want C++ to have named/parameterized statement expressions at all, let alone templates of them.
I understand you don't want these. I'd like you to understand that many, including myself, do. Thus it - sooner or later - gets implemented (read: I'm already looking into how to do it on Itanium ABI); and if it's not standardized, it gets implemented multiple ways by multiple vendors. That's a nightmarish vision we want to avoid.

Are GCC and Clang developers implementing "parameterized named statements" as a compiler feature? If not, then you seem to be talking about a hypothetical, not something that's actually happening.

Hence I proposed these as optional features. I do not ask you to 'vote for accepting it', I ask you to consider these under the 'don't have to do this, but if you do, do it this way'-rule.
Note that, by having them as inline templates (or similar), they are still 'macro-like', but with the benefit of type safety, ADL, namespaces and no `#undef' and redefining (which I actually consider a benefit).

How is there type-safety? How is there ADL or namespaces? All of those things come from the cite of usage, not the cite of definition. What can the compiler do except convert them into a bunch of tokens to be pasted in at the right place?

Remember: the whole point of these statements is that they have direct access to the outer scope. As such, the meaning of potentially every identifier could be radically different. It's basically templates, only much, much harder.

Consider something as simple as this:

named_statement stmt() = ({produce foo(thingy);});

So... what does the compiler do with `stmt`? What does that mean? Well, you could use it in numerous circumstances:

//Case 1
int foo(int);
int thingy = ...;
stmt();

//Case 2
struct bar
{
  int foo(int);

  void func()
  {
    int thingy = ...;
    stmt();
  }
};

//Case 3
struct bar
{
  int foo(int);

  void func()
  {
    stmt();
  }

  int thingy;
};

//Case 4
struct foo{foo(int); ... };
int thingy = ...;
stmt();

In case 1, `stmt` resolves to calling a non-member function named `foo` with a local variable named `thingy`, resulting in an `int`.

In case 2, `stmt` resolves to calling a member function of `bar` named `foo`, using a local variable named `thingy`, resulting in an `int`.

In case 3, `stmt` resolves to calling a member function of `bar` named `foo`, using a member variable named `thingy`, resulting in an `int`.

In case 4, `stmt` resolves to a prvalue of type `foo`, direct initialized from a variable named `thingy`.

Given just the definition of `stmt`, what can the compiler reasonable do to that which can result in all of those scenarios being legal C++? The only thing it can do is some basic token recognition and storage, to be regurgitated later when `stmt` is invoked.

So where do you get "type safety, ADL, namespaces" from? Because `stmt` knows nothing about those things. The call to `foo` may use ADL or be a member function. Hell, as I pointed out, it may not even be a call; it may construct a type with that name. So where is the safety here? The compiler can't know what `foo(thingy)` is, since what it resolves to is entirely context dependent.

Oh sure, there are some things it might be able to detect that macros cannot. It knows that `if for else` is not legit code, while a macro can't tell you that until you actually use it. You can't piece together fragments of statements; the statements have to be whole and complete on their own.

But otherwise, the compiler has no idea what is going on. So I really don't see the benefits to this over a macro.

Templates are explicit restricted, which makes it possible for the compiler to have an inkling as to what is going on. In a template, you say explicitly what kind of thing the template parameters are: values, typenames, or templates. And dependent names have to be explicitly qualified, so that the compiler understands what's going on.

A template may invoke different functions depending on its template arguments. But explicit specializations aside, it cannot do radically different things with different parameters. An expression that calls a function always calls a function; it never sometimes does direct initialization.

The only way statement expressions can work is if the cite of their declaration is also the cite of their invocation.

If the issue you see is with some features of functions/templates/lambdas that macros don't have or do much differently, please help me understand and I'll try to come up with an idea that solves both these issues and the above-mentioned benefits.

I don't understand what you mean here.

Nicol Bolas> I would also suggest that motivations besides "wrap it in a macro" be specified. The current motivation and examples all suggest that this feature exists solely for the benefit of macro programming.
If you could elaborate a bit on this, I'd be happy to fix these parts. For me, the non-macro-like use case would be the parametric one, but I feel you're thinking of something else.

Any form of complex variable initialization would qualify. Have you ever written a block of code who's sole purpose was to compute a value/object? One that perhaps created a few local variables that nobody else ever touched? But it's a one-off, so there's no point in making it a function. And maybe it needs local variables, so that would require you to forward them as parameters and other hassles; it'd be much clearer to just do it here.

Have you ever needed to initialize a class member with something that was too complex to be an expression, so you used a lambda to compute the value? This is also a common idiom.

To me, statement expressions are all about complex ways to compute a value. Sometimes, that computation ends in failure (`return`, `break`, `throw`, etc). But ultimately, the point of using them is to get a value. They have their own scope, but they're just as much a part of the outer scope as anything else. So they can access the outer scope just like any other scope.

You seem to want them to make macro writers' lives easier, so that they can isolate their code from the outside, so that they can affect the outer scope in some way, etc.

Viacheslav Usov

unread,
Feb 9, 2017, 5:28:56 AM2/9/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Frankly, I do not really understand why we would want this.

Without the control statements, statement expressions are not different from lambdas as I remarked earlier.

So it is really flow control inside expressions that is being proposed. But the only motivating example given is a way to write a macro. Examples given further, up to the parametric SE, are also focused on macros. So, just given the proposal (up to PSE), we are talking about a language feature to support macro-programming. That kind of motivation seems strange to me.

Now, the parametric stuff. The essence of that is a non-local transfer of control from arbitrary functions. That is akin to exceptions, and, given the story of noexcept, we have firmly established that we need means of establishing hard guarantees of no non-local transfer of control. Now, if we introduce another non-local transfer mechanism, we will also introduce the same pile of problems that noexcept has dealt with.

Cheers,
V.

Nicol Bolas

unread,
Feb 9, 2017, 10:32:36 AM2/9/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
On Thursday, February 9, 2017 at 5:28:56 AM UTC-5, Viacheslav Usov wrote:
Frankly, I do not really understand why we would want this.

Without the control statements, statement expressions are not different from lambdas as I remarked earlier.

Ignoring return/break/continue, they are different from lambdas in several ways. They're much cleaner, for example. It's hard to recognize the difference between:

auto i = [&] {
 
//several
 
//lines
 
//of
 
//code
};

And:

auto i = [&] {
 
//several
 
//lines
 
//of
 
//code
}();

With a statement expression, the distinction becomes quite clear.

Also, manipulating scope-accessible variables does not require you to say `[&] mutable`, as it would with a lambda.
 
So it is really flow control inside expressions that is being proposed. But the only motivating example given is a way to write a macro. Examples given further, up to the parametric SE, are also focused on macros. So, just given the proposal (up to PSE), we are talking about a language feature to support macro-programming. That kind of motivation seems strange to me.

Now, the parametric stuff. The essence of that is a non-local transfer of control from arbitrary functions. That is akin to exceptions, and, given the story of noexcept, we have firmly established that we need means of establishing hard guarantees of no non-local transfer of control.

That's not the point of noexcept. The point of noexcept is to allow code to detect when a process may not fail and thus potentially use more efficient algorithms. `variant` implementations benefit from this greatly.

Issuing a non-local return/break/continue through code would not be the same thing.

Now, I highly despise the idea of passing around functions that can non-local jump out of code. But my concern is not about `noexcept` and so forth. It's about the ease with which such things can be broken. Adding an exception-like mechanism for them will not change how easy they are to break.

Viacheslav Usov

unread,
Feb 9, 2017, 10:48:05 AM2/9/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
On Thu, Feb 9, 2017 at 4:32 PM, Nicol Bolas <jmck...@gmail.com> wrote:

> With a statement expression, the distinction becomes quite clear.

True, and I almost proposed some nicer syntax for that some time ago, but we are ultimately talking about a three char difference.

> That's not the point of noexcept. The point of noexcept is to allow code to detect when a process may not fail and thus potentially use more efficient algorithms. `variant` implementations benefit from this greatly.

"May not fail" is just a sub-set of "no non-local transfer of control".

> Issuing a non-local return/break/continue through code would not be the same thing.

Yeah, by definition, if so defined. Functionally, though, one could implement all that non-local stuff with exceptions today.

Because functionally they are really similar. I singled out noexcept simply because its very existence is tangible evidence that we want very strict control over non-local transfer of control. I agree that all that non-local stuff adds a lot more complexity than we might want to deal with, in more than just one way.

Cheers,
V.

Thiago Macieira

unread,
Feb 9, 2017, 1:05:17 PM2/9/17
to std-pr...@isocpp.org
On quinta-feira, 9 de fevereiro de 2017 07:32:36 PST Nicol Bolas wrote:
> auto i = [&] {
> //several
> //lines
> //of
> //code
> };
>
> And:
>
> auto i = [&] {
> //several
> //lines
> //of
> //code
> }();

Easy solution: use the actual return type, instead of auto.

szollos...@gmail.com

unread,
Feb 9, 2017, 4:39:37 PM2/9/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Hi,


2017. február 9., csütörtök 3:56:59 UTC+1 időpontban Nicol Bolas a következőt írta:

Um, why can't you have RAII? That's like saying that you can't have RAII if you use a lambda to mimic expression statements.

I hope we can, and I agree with the above implementation if Intel can be concieved.
 
Consider something as simple as this:

named_statement stmt() = ({produce foo(thingy);});

So... what does the compiler do with `stmt`? What does that mean?
Ok, I see your point much clearer now and I can assure you, I don't want the things you mentioned (neither in this proposal nor the next). I'm actually going for something much cleaner and lightweight with named SE; thus, I think I should rewrite this part. My plan is to make the above a compile-time error outright. The reason it's a compile-time error is that `foo' and `thingy' are not defined at the point of definition. If you want a named SE / parametric SE that has parameters called `foo' and `thingy' (as opposed to capture, which we normally do), you naturally have to specify it (along with type) in the function argument list:

template<typename F, typename T>
auto namedstmt(F&& foo, T&& thingy)
/* -> decltype(foo(thingy)) */ // if needed
({ produce foo(thingy); })
}

auto paramstmt = [&](auto&& foo, auto&& thingy)
/* -> decltype(foo(thingy)) */ // if needed
({ produce foo(thingy); })

Note that this (the lambda-like parametric version) still allows for capturing in the defining context:


int foo(int);

auto paramstmt = [&](auto&& thingy)
/* -> decltype(foo(thingy)) */ // if needed
({ produce foo(thingy); })
 
Note that we have perfect type safety from the point the named / parametric SE appears in the code. I do think that we want very similar things: when you say 'appear only once', I translate it to 'capture only once'. After all, calling these kind of expressions is not much different from building a trampoline and placing them in a loop.

As for use cases: for anything that doesn't need control stmts, I have to admit that I'm currently using lambdas. I also agree, had SE been standardized, I used them all the time instead of `[&]{ ... }()'; not because of the 5 chars, but because it's cleaner.


You seem to want them to make macro writers' lives easier, so that they can isolate their code from the outside, so that they can affect the outer scope in some way, etc.
Quite the contrary: I'm planning to estabilish a language construct that avoids macros. Indeed, I have to fix the proposal in many points: I only introduced named / parametric SE in the end, therefore there are only macro-based examples before. This needs to be fixed. I also want to point out how it can be used without involving macros (both simple SE and named / parametric SE).

szollos...@gmail.com

unread,
Feb 9, 2017, 5:00:36 PM2/9/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
...

Hi,


Now, the parametric stuff. The essence of that is a non-local transfer of control from arbitrary functions. That is akin to exceptions, and, given the story of noexcept, we have firmly established that we need means of establishing hard guarantees of no non-local transfer of control. Now, if we introduce another non-local transfer mechanism, we will also introduce the same pile of problems that noexcept has dealt with.
My understanding is that the problem with exceptions is that a function (unless `noexcept()') can throw anything, therefore control might be taken to any other (outer) catch block dynamically. Neither of these three apply to any SE, not even to my other idea on the "capturing monads..." thread (`throw inline / catch inline'):
  • we can tell exactly which kind of control transfers can happen (because SE source must be visible, thus you see the captured control stmts);
  • at this point, only the caller is affected (in the "capturing monads..." thread this is different, but still the exact type -> catch is deducible compile-time or in IDE);
  • all of these bind compile-time, whereas exceptions (as of today) bind runtime, which is their greatest limiting factor.
To put it another way, probably noone complained about a function that returns `std::variant<return_type, exception_types...>'. In fact, SE moves in this direction, as the variant is easily checked / delegated for visitation inside and the code logic is kept intact.

Thanks,
-lorro

szollos...@gmail.com

unread,
Feb 9, 2017, 5:30:14 PM2/9/17
to ISO C++ Standard - Future Proposals
...

Hi,


2017. február 9., csütörtök 19:05:17 UTC+1 időpontban Thiago Macieira a következőt írta:
On quinta-feira, 9 de fevereiro de 2017 07:32:36 PST Nicol Bolas wrote:
> auto i = [&] {
>   //several
>   //lines
>   //of
>   //code
> };
>
> And:
>
> auto i = [&] {
>   //several
>   //lines
>   //of
>   //code
> }();

Easy solution: use the actual return type, instead of auto.
 Sir, that's indeed doable for simple types, but consider the following:
struct Desc;
std::map<std::string, Desc> sn2desc;

std::set<std::pair<std::string, std::string>> nNsnS;
const std::string k;

//
make_filter_first returns a custom range that walks over
// ->second where ->first is k
; I used this recently
// Disclaimer: don't do this with boost, that's UB.
auto
descs = [&]() {
    return
make_filter_first(ps, k)
           |
transformed([](const std::string& sn) => sn2desc[sn])
           | filtered([](const Desc& desc) => desc.accepted_);
}();

Do you really want to write the type it returns? If I were to, I'd duplicate the body and try find to the typos...

Thanks,
-lorro

Nicol Bolas

unread,
Feb 9, 2017, 6:42:26 PM2/9/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
On Thursday, February 9, 2017 at 4:39:37 PM UTC-5, szollos...@gmail.com wrote:
Hi,

2017. február 9., csütörtök 3:56:59 UTC+1 időpontban Nicol Bolas a következőt írta:

Um, why can't you have RAII? That's like saying that you can't have RAII if you use a lambda to mimic expression statements.

I hope we can, and I agree with the above implementation if Intel can be concieved.

Who cares what Intel says or doesn't say? The goal isn't to take some intersection of random stuff from separate compilers. The goal is to make a functioning feature which is standard across compilers. If that means Intel has to do some work, then they have to do some work.

Just like Microsoft had to practically rebuild their front-end to make `constexpr`, expression SFINAE, and other C++11 features work (not to mention two-phase lookup which they still haven't done).

Consider something as simple as this:

named_statement stmt() = ({produce foo(thingy);});

So... what does the compiler do with `stmt`? What does that mean?
Ok, I see your point much clearer now and I can assure you, I don't want the things you mentioned (neither in this proposal nor the next). I'm actually going for something much cleaner and lightweight with named SE; thus, I think I should rewrite this part. My plan is to make the above a compile-time error outright. The reason it's a compile-time error is that `foo' and `thingy' are not defined at the point of definition. If you want a named SE / parametric SE that has parameters called `foo' and `thingy' (as opposed to capture, which we normally do), you naturally have to specify it (along with type) in the function argument list:

template<typename F, typename T>
auto namedstmt(F&& foo, T&& thingy)
/* -> decltype(foo(thingy)) */ // if needed
({ produce foo(thingy); })
}

auto paramstmt = [&](auto&& foo, auto&& thingy)
/* -> decltype(foo(thingy)) */ // if needed
({ produce foo(thingy); })


OK, so you don't actually want statement expressions. What you want are a slightly modified of function that can magically invoke `return/break/continue` into their calling contexts. Presumably through multiple layers of such functions.

Because right now, that seems to be the only difference between a named SE and a function.

You also need to explain exactly what kind of error you get if `break` or `continue` isn't legal from the context in which a statement function is called.

I'm starting to agree with Viacheslav here. This is starting to seem very much like a modified form of exception handling. Essentially, you're saying `throw return value` or `throw break` or whatever. And the compiler catches it at certain places and does that.

Nicol Bolas

unread,
Feb 9, 2017, 6:53:14 PM2/9/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
On Thursday, February 9, 2017 at 5:00:36 PM UTC-5, szollos...@gmail.com wrote:
...

Hi,

Now, the parametric stuff. The essence of that is a non-local transfer of control from arbitrary functions. That is akin to exceptions, and, given the story of noexcept, we have firmly established that we need means of establishing hard guarantees of no non-local transfer of control. Now, if we introduce another non-local transfer mechanism, we will also introduce the same pile of problems that noexcept has dealt with.
My understanding is that the problem with exceptions is that a function (unless `noexcept()') can throw anything, therefore control might be taken to any other (outer) catch block dynamically.

No, that is a problem, not "the problem."

`noexcept` exists so that code can detect if certain operations are guaranteed to be able to complete. And if they are, then more efficient ways of doing them can be used based on that certainty.
 
Neither of these three apply to any SE, not even to my other idea on the "capturing monads..." thread (`throw inline / catch inline'):
  • we can tell exactly which kind of control transfers can happen (because SE source must be visible, thus you see the captured control stmts);
  • at this point, only the caller is affected (in the "capturing monads..." thread this is different, but still the exact type -> catch is deducible compile-time or in IDE);
  • all of these bind compile-time, whereas exceptions (as of today) bind runtime, which is their greatest limiting factor.
To put it another way, probably noone complained about a function that returns `std::variant<return_type, exception_types...>'. In fact, SE moves in this direction, as the variant is easily checked / delegated for visitation inside and the code logic is kept intact.

Java has proven that this is a bad idea. It's essentially forcibly checked exceptions, where every layer of a function must either handle the exception or explicitly name it as an exception that gets thrown. Just because you put the "exception_types" in the function's return value instead of its exception specification list does not change all of the problems with such things.

So yes, people do complain about functions that return such nonsense. `expected` is one thing. A giant variant with one good value and a bunch of arbitrary errors is a function that, instead of using C++'s standard error handling mechanism, has decided to make everyone else live with its unwillingness to clean up its own dirt.

This is not a construct we want to proliferate in C++.

Thiago Macieira

unread,
Feb 9, 2017, 8:35:31 PM2/9/17
to std-pr...@isocpp.org
On quinta-feira, 9 de fevereiro de 2017 14:30:13 PST szollos...@gmail.com
wrote:
> Sir, that's indeed doable for simple types, but consider the following:
[cut]
> Do you really want to write the type it returns? If I were to, I'd
> duplicate the body and try find to the typos...

It's your choice. You can choose to write the actual type and know that the
lambda is being run, or you can look at the trailing end to see the ().

In my opinion, use of auto should be avoided, so that silly mistakes when
refactoring code don't change expected behaviour.

Nicol Bolas

unread,
Feb 9, 2017, 9:03:41 PM2/9/17
to ISO C++ Standard - Future Proposals


On Thursday, February 9, 2017 at 8:35:31 PM UTC-5, Thiago Macieira wrote:
On quinta-feira, 9 de fevereiro de 2017 14:30:13 PST szollos...@gmail.com
wrote:
>  Sir, that's indeed doable for simple types, but consider the following:
[cut]
> Do you really want to write the type it returns? If I were to, I'd
> duplicate the body and try find to the typos...

It's your choice. You can choose to write the actual type and know that the
lambda is being run, or you can look at the trailing end to see the ().

Or we can just have a way to do complex forms of object initialization.

If we have a lot of people (ab)using lambdas to do object initialization (and we do), that should be seen as a sign of a language deficiency.

In my opinion, use of auto should be avoided, so that silly mistakes when
refactoring code don't change expected behaviour.

By that logic, we shouldn't have `auto` at all.

Does it really matter exactly what type gets returned? No. What matters is how you use that type. Whether the object it creates fits the code that uses it. Sure, people can make "silly mistakes" when refactoring with `auto` use. But pervasive use of `auto` for complex types also means that you get to change return types without breaking people's code.

After all, they don't care what object it returns; they care whether the object can do what they're going to do with it.

Thiago Macieira

unread,
Feb 10, 2017, 12:37:16 AM2/10/17
to std-pr...@isocpp.org
Em quinta-feira, 9 de fevereiro de 2017, às 18:03:41 PST, Nicol Bolas
escreveu:
> > In my opinion, use of auto should be avoided, so that silly mistakes when
> > refactoring code don't change expected behaviour.
>
> By that logic, we shouldn't have `auto` at all.

I said avoided, not eliminated. There are good uses for auto, especially when
the type is used on that same line (new and static_cast, for example).

That and iterators are the only allowed uses of auto in Qt source code.

> Does it *really* matter exactly what type gets returned? No. What matters
> is *how you use that type*. Whether the object it creates fits the code
> that uses it. Sure, people can make "silly mistakes" when refactoring with
> `auto` use. But pervasive use of `auto` for complex types *also* means that
> you get to change return types without breaking people's code.
>
> After all, they don't care what object it returns; they care whether the
> object can do what they're going to do with it.

Sounds like a job for Concepts.

szollos...@gmail.com

unread,
Feb 10, 2017, 4:01:06 AM2/10/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Hi,


2017. február 10., péntek 0:42:26 UTC+1 időpontban Nicol Bolas a következőt írta:
 `return/break/continue` into their calling contexts. Presumably through multiple layers of such functions.
There are no multiple layers. `return' inside a SE means return from the evaluating context, not from the defining context.
 
You also need to explain exactly what kind of error you get if `break` or `continue` isn't legal from the context in which a statement function is called.
Compile-time diagnostic. At the point of call it can be decided if the named / parametric SE can be called. Yes, I do want to make it a part of the type of `operator()' (or whatever mechanics we'll use to switch to the return / break / continue continuations passed after destructing the current one).
Think of it this way: `return' in a SE does the following:
- cleanup (by SE): destruct automatic storage duration variables not needed in the return statement
- delegated return: jump to the cleanup (and then, return) code of the caller
If you can 'limit the scope of' (read: ensure destruction of) ASDVs mentioned in cleanup (which the compiler can do) and have TCO, the second could essentially be a `[[noreturn]]' function implemented by the compiler in assembly. So we're specifying a `[[noreturn]]' function at the calling point.
 
I'm starting to agree with Viacheslav here. This is starting to seem very much like a modified form of exception handling. Essentially, you're saying `throw return value` or `throw break` or whatever. And the compiler catches it at certain places and does that.
No, `throw' propagates through multiple 'stack frames' (yep, calling contexts). This does not. This always return / break / etc. from the caller.

Thanks,
-lorro

Viacheslav Usov

unread,
Feb 10, 2017, 5:03:50 AM2/10/17
to ISO C++ Standard - Future Proposals, Lorand Szollosi
On Thu, Feb 9, 2017 at 11:00 PM, <szollos...@gmail.com> wrote:

> My understanding is that the problem with exceptions is that a function (unless `noexcept()') can throw anything, therefore control might be taken to any other (outer) catch block dynamically.

For any code that calls a noexcept functor, it does not matter where the control will be taken from. That code expects that the functor returns normally, or the entire abstract machine gets terminated.

> we can tell exactly which kind of control transfers can happen (because SE source must be visible, thus you see the captured control stmts);

False. The code calling a functor that is actually a PSE has no idea where the control is transferred to. As I said just above, that code may well expect that the control may not be transferred anywhere at all.

Your example used std::accumulate(). The latter definitely does not know anything about your PSE and the context it was defined in,

Cheers,
V.

Vittorio Romeo

unread,
Feb 10, 2017, 9:54:19 AM2/10/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Just wanted to say that I really really like this proposal. I feel like that there's way too much syntactical overhead when dealing with perfectly-forwarded captures and trailing return types (as seen in the `curry` example). Hopefully you'll submit it and it will be well-received.

On Tuesday, 7 February 2017 01:57:56 UTC, szollos...@gmail.com wrote:
Hi,

Please find below the initial draft for a statement expression proposal. Any ideas, hints, suggestions are welcome. I understand that it's my first proposal and it might take several iterations if ever to make if fly, so I'd be very glad about points where I could make it better (one of them is formatting, which I'm going to fix); whether to add more detail or less; whether to change examples.

Link to the proposal's initial draft:
http://lorro.hu/cplusplus/statement_expressions.html

Thanks in advance,
-lorro

Nicol Bolas

unread,
Feb 10, 2017, 10:33:48 AM2/10/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
On Friday, February 10, 2017 at 4:01:06 AM UTC-5, szollos...@gmail.com wrote:
Hi,

2017. február 10., péntek 0:42:26 UTC+1 időpontban Nicol Bolas a következőt írta:
 `return/break/continue` into their calling contexts. Presumably through multiple layers of such functions.
There are no multiple layers. `return' inside a SE means return from the evaluating context, not from the defining context.

And where exactly is that? If you call a named statement, and that named statement does a `for` loop in which it calls another named statement, then the inner statement ought to be able to `break` from the outer named statement, not into the ultimate calling function. So there very much are stack frames here.

Stack frames of named statements.

If the inner statement issues a `return`, then it must terminate both the inner and outer named statements. Exactly like exception handling.

Thus far, the only differences between this and exceptions are:

1: Presumably, named statements have to be defined where they are declared. That is, they're always inline.

2: Presumably, named statements have no function pointer analog; you can't pass them around and so forth. This makes the resolution of their control structures a static property of the code.

It should also be noted that your focus on named statements removes much of the purpose of in-function statement expressions. If a SE cannot access stuff in the function it is defined within, then you can't really use it for complex object initialization.

So it seems that there are two separate features: named statements and statement expressions. They might share some syntactic elements, but they really do need to act like distinct constructs.

Nicol Bolas

unread,
Feb 10, 2017, 10:43:03 AM2/10/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com

That's a good point. The example in the proposal is this (preserved in case it gets changed):

template<typename R>
int product_of_range(const R& r)
{
   
return std::accumulate(begin(r), end(r), 1, [&](int lhs, int rhs) ({
       
if (rhs == 0) return 0;
        lhs
* rhs;
   
}));
}

This code strongly suggests that you can force another function to execute a statement expression. That is, the functor passed to `accumulate` isn't a lambda; it's a statement expression object of some kind.

Once you can pass a statement expression around, once you can make a function execute a statement expression without knowing that it is doing so, you lose the static ability to determine what's going on in your code.

Not to mention the potential behavior with regard to return type deduction. If a parameter to a function can affect return type deduction (by issuing a return from a statement expression early), then figuring out what the function's return type will be becomes untenable. It's bad enough having to scan through a function to work out its return type.

Which of course gives us the possibility of a named statement passed to a function hijacking a function's return type. That is, using the rules of return type deduction to force it to return a different-yet-compatible type. That could break all kinds of stuff.

So passing around named statements and invoking them like functions? No.
Message has been deleted

Nicol Bolas

unread,
Feb 10, 2017, 11:00:16 AM2/10/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com

Not to mention, `std::accumulate` itself doesn't have to directly call the functor. It can call a sub-function which calls the functor. Or a sub-function of a sub-function calls the functor. It may even have an internal lambda that does the calling. Who knows?

The only way to make that named statement behave the way you want it to (terminate `std::accumulate` itself, forcing it to `return 0`) is to effectively link the named statement to `std::accumulate`. But even if that were possible, you would still have to deal with the fact that you're returning through all of those potential sub-functions.

So now we have a stack frame that contains actual functions, not just statement expressions. So how exactly is this different from an exception?

szollos...@gmail.com

unread,
Feb 12, 2017, 4:47:36 PM2/12/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Hi,

Thanks again for the feedback.

Viacheslav Usov> The code calling a functor that is actually a PSE has no idea where the control is transferred to. As I said just above, that code may well expect that the control may not be transferred anywhere at all.

Currently we don't have noexcept for PSEs and it's not clear what it'd mean to have that, given you consider non-local transfer as an exception from this point-of-view.. If you have a function that calls a PSE that calls a PSE, then we have the guarantee that (unless by an actual throw) both the inner and the outer PSE can at most cause the outer function to return. Therefore I think that the function might still be noexcept. This is different from an exception which might propagate outside that function.

Viacheslav Usov> Your example used std::accumulate(). The latter definitely does not know anything about your PSE and the context it was defined in,

It does not know the context, similarly that it does not know the context of a lambda. std::accumulate() accepts a template type. That template type has a (possibly new kind of) template operator(), which is resolved in std:accumulate() automatically by its `return'. Thus it does know about my PSE, at least to the level that it knows that it has to resolve the template return. Naturally this also means that PSEs are inline.

Nicol Bolas> If you call a named statement, and that named statement does a `for` loop in which it calls another named statement, then the inner statement ought to be able to `break` from the outer named statement, not into the ultimate calling function. So there very much are stack frames here.

This is the same for SE, PSE and NSE. Stack frames are implementation, not standard, therefore - so far - I've tried to avoid them in the explanation. If it's easier to discuss in terms of implementations, then a function / functor receives an implicit return continuation (=return address) on the stack; a PSE might receive up to a return, break, continue and produce continuation. I do look at normal SEs as function-like objects that take these - this might be a difference in our p.o.v. - i.e., in my model any SE is CALLed and uses RET for returning. Then the optimizer can remove these if unnecessary. But that's just implementation.

Nicol Bolas> If the inner statement issues a `return`, then it must terminate both the inner and outer named statements. Exactly like exception handling.

Not exactly: the exception might propagate out of any number of functions. Inline return propagates only to the caller of the function that called the SE/PSE/NSE. Thus a caling function can still be noexcept, even if we consider non-local return as an exception-like entity from the PSE. It might be exception-like from that point, but from the calling function, it's a normal return (with the same return type). So I wouldn't consider that as breaking the noexcept contract: the PSE-calling function's caller will continue and can't distinguish (if not by value) if the callee or the inner PSE returned.

Nicol Bolas> If a SE cannot access stuff in the function it is defined within, then you can't really use it for complex object initialization.

A PSE can. Granted, dangling references or even UB might occur if you somehow return such a PSE outside the context of captures; but the same happens with lambdas. If I don't understand you correctly, could you please help me understand what's missing?

Nicol Bolas> This code strongly suggests that you can force another function to execute a statement expression. That is, the functor passed to `accumulate` isn't a lambda; it's a statement expression object of some kind.

Yep, in my model it's passed similarly as a lambda (Note: I say 'passed', not 'called'). Sorry that I took this evident, that's one more point I should explain in the text. To me, a PSE is defined at a point (where it might have captures, but it doesn't capture continuations there); then it can be passed around as an object of a specific type. It's presumably non-copiable, it might be movable. It has a template operator(), which is special. Had `return' (and the other control flow statements) been a [[noreturn]] functor, it might have been just an ordinary template parameter. Since it's not (and since we want auto-resolve to avoid ugly syntax), it's part of the template specification, but not a type parameter. This is similar to `...' being part of the argument list, but not being a function argument. Thus it can only be resolved in a context where that given keyword is actually allowed. Technical implementation might require a stack frame - or just inlining.

Nicol Bolas> Once you can pass a statement expression around, once you can make a function execute a statement expression without knowing that it is doing so

Good point. If you think it's preferable, I might come up with a solution to detect if `T' is a PSE/NSE. Note however that you (or your compiler) still have the static ability to decide what's going on: all PSEs/NSEs must be inlined and can't be hidden in a library or behind a virtual ptr.

Nicol Bolas> Not to mention the potential behavior with regard to return type deduction. If a parameter to a function can affect return type deduction (by issuing a return from a statement expression early), then figuring out what the function's return type will be becomes untenable

I understand your concern, but in my model it works the other way around. Assume a function f calling a PSE e. Then the return type of f is decided first (name it RET); then the compiler, at each point of call with args..., tries decltype(e)::operator<RET>()(args...). If it's invalid, then that's a compile-time error. Thus you can't have a PSE that produces int but might return nullopt in a function that would otherwise (read: w/o the PSE's `return') return int. Return types are not affected - not conforming them means invalid code.

Nicol Bolas> Not to mention, `std::accumulate` itself doesn't have to directly call the functor. It can call a sub-function which calls the functor. Or a sub-function of a sub-function calls the functor. It may even have an internal lambda that does the calling. Who knows?

Specification. I'm okay with any number of nestings in `std::accumulate' as long as returning from the innermost nest allows me to have identical behaviour as the flat version. In other words, a PSE can only be used if you have (valid, specified) assumptions about the internals of the function you call. If you don't, you don't pass a PSE. It's just a feature. It's useful when you're responsible for both the function and the PSE (which is very oft) or when there's a clear specification of what early return does. Note that you need to have specifications about how your lambda is called as well.
If you wanted the other kind of guarantee, namely across several function calls, then I'm considering inline catch as a separate proposal (which is akin to take_return in the other thread; that is an exception, albeit a quick one, resolved in compile-time, but still non-local).

Thanks,
-lorro

szollos...@gmail.com

unread,
Feb 12, 2017, 4:57:58 PM2/12/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Hi,

Correction: tries decltype(e)::operator<return RET>()(args...)

Thanks,
-lorro

Nicol Bolas

unread,
Feb 12, 2017, 8:41:23 PM2/12/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
On Sunday, February 12, 2017 at 4:47:36 PM UTC-5, szollos...@gmail.com wrote:
Hi,

Thanks again for the feedback.

Viacheslav Usov> The code calling a functor that is actually a PSE has no idea where the control is transferred to. As I said just above, that code may well expect that the control may not be transferred anywhere at all.

Currently we don't have noexcept for PSEs and it's not clear what it'd mean to have that, given you consider non-local transfer as an exception from this point-of-view.. If you have a function that calls a PSE that calls a PSE, then we have the guarantee that (unless by an actual throw) both the inner and the outer PSE can at most cause the outer function to return. Therefore I think that the function might still be noexcept. This is different from an exception which might propagate outside that function.

Here's a simple function:

template<typename F>
int caller(F f)
{
  T *t = new T(...);
  f();
  delete t;
  return 5;
}

Now, this has an obvious hole: if `f` throws, then you leak memory. So, we do this:

template<typename F>
int caller(F f)
{
  T *t = new T(...);
  try{
    f();
  }
  catch(...)
  {
    delete t;
    throw;
  }
  delete t;
  return 5;
}

That code is now perfectly fine. Stupid obviously, but perfectly fine.

Named statements break this code. If `f` forces a `return` on `caller` early, `caller` becomes broken.

Your response might be to say, "Just use RAII". And while that might fix this simple example, that's not a guarantee. There's a reason why `catch(...)` exists. There will always be circumstances where some things aren't/can't be wrapped in objects.

You might say that you could use `scope_guard` or similar constructs. And that might be adequate, if not for one problem: the above code works right now. By creating this construct, you are now forcing people to consider an eventuality that they never considered before: that calling what by all appearances is a function may cause their function to exit without an exception.

A function can only terminate in 3 ways: `std::terminate` and its ilk, `throw`, and `return`. Until now, `return` has always been local; unlike the other two, someone cannot `return` for you. So any code written with the expectation that a function cannot return for them is now potentially broken.

I consider it conceptually rude to be able to impose a `return` on a function. If that function asks to allow you to return for it, that's fine. But it should be something the function explicitly is written to permit. It should never be hidden and it should never be imposed upon you without your will, knowledge, or consent.

Note that having an explicit syntax at the cite of the call would give you a chance to solve the `accumulate` problem. If the function knows that something could be a named statement, then there could be "passthrough" syntax which means "if a named statement invoked `return`, then invoke it on my caller instead". Therefore, standard library functions could be redefined such that they will call their functors using that passthrough syntax.

Viacheslav Usov> Your example used std::accumulate(). The latter definitely does not know anything about your PSE and the context it was defined in,

It does not know the context, similarly that it does not know the context of a lambda. std::accumulate() accepts a template type. That template type has a (possibly new kind of) template operator(), which is resolved in std:accumulate() automatically by its `return'. Thus it does know about my PSE, at least to the level that it knows that it has to resolve the template return. Naturally this also means that PSEs are inline.

Nicol Bolas> If you call a named statement, and that named statement does a `for` loop in which it calls another named statement, then the inner statement ought to be able to `break` from the outer named statement, not into the ultimate calling function. So there very much are stack frames here.

This is the same for SE, PSE and NSE. Stack frames are implementation, not standard, therefore - so far - I've tried to avoid them in the explanation. If it's easier to discuss in terms of implementations, then a function / functor receives an implicit return continuation (=return address) on the stack; a PSE might receive up to a return, break, continue and produce continuation. I do look at normal SEs as function-like objects that take these - this might be a difference in our p.o.v. - i.e., in my model any SE is CALLed and uses RET for returning. Then the optimizer can remove these if unnecessary. But that's just implementation.

What you call them is irrelevant: function-like objects, "CALLed and RET" whatever. What matters is behavior. And that behavior is very much the same as exception resolution: keep going up the graph of function calls until you find the right place.

The only difference seems to be how "the right place" is defined. With exceptions, it's defined explicitly. With named statements, it happens a different way.

Nicol Bolas> If the inner statement issues a `return`, then it must terminate both the inner and outer named statements. Exactly like exception handling.

Not exactly: the exception might propagate out of any number of functions.

And the `return` may propagate out of any number of named statement functions. What's the difference?
 
Inline return propagates only to the caller of the function that called the SE/PSE/NSE.

An exception only propagates to the caller who has a matching `catch` statement. So again, what's the difference? `try/catch` blocks are even a static property of a function.
 
Thus a caling function can still be noexcept, even if we consider non-local return as an exception-like entity from the PSE. It might be exception-like from that point, but from the calling function, it's a normal return (with the same return type). So I wouldn't consider that as breaking the noexcept contract: the PSE-calling function's caller will continue and can't distinguish (if not by value) if the callee or the inner PSE returned.

You may be confusing the two arguments here. Viacheslav Usov is the one who believes that this feature "breaks the noexcept contract". I do not. I didn't say anything about `noexcept`.

Nicol Bolas> This code strongly suggests that you can force another function to execute a statement expression. That is, the functor passed to `accumulate` isn't a lambda; it's a statement expression object of some kind.

Yep, in my model it's passed similarly as a lambda (Note: I say 'passed', not 'called'). Sorry that I took this evident, that's one more point I should explain in the text. To me, a PSE is defined at a point (where it might have captures, but it doesn't capture continuations there); then it can be passed around as an object of a specific type. It's presumably non-copiable, it might be movable.

Then perhaps you should take another look at `std::accumulate`'s definition; it takes `BinaryOp` by value. So copying/moving is gonna happen. Most standard library functors work in this fashion.

It has a template operator(), which is special. Had `return' (and the other control flow statements) been a [[noreturn]] functor, it might have been just an ordinary template parameter. Since it's not (and since we want auto-resolve to avoid ugly syntax), it's part of the template specification, but not a type parameter. This is similar to `...' being part of the argument list, but not being a function argument. Thus it can only be resolved in a context where that given keyword is actually allowed. Technical implementation might require a stack frame - or just inlining.

Nicol Bolas> Once you can pass a statement expression around, once you can make a function execute a statement expression without knowing that it is doing so

Good point. If you think it's preferable, I might come up with a solution to detect if `T' is a PSE/NSE. Note however that you (or your compiler) still have the static ability to decide what's going on: all PSEs/NSEs must be inlined and can't be hidden in a library or behind a virtual ptr.

Nicol Bolas> Not to mention the potential behavior with regard to return type deduction. If a parameter to a function can affect return type deduction (by issuing a return from a statement expression early), then figuring out what the function's return type will be becomes untenable

I understand your concern, but in my model it works the other way around. Assume a function f calling a PSE e.

Stop. The situation we're talking about is that you have a function `f` which has been given a function object `e`. You, as the writer of `f`, have no clue that `e` is a named statement. `f` just so happens to be using return type deduction.

Then the return type of f is decided first (name it RET); then the compiler, at each point of call with args..., tries decltype(e)::operator<RET>()(args...). If it's invalid, then that's a compile-time error. Thus you can't have a PSE that produces int but might return nullopt in a function that would otherwise (read: w/o the PSE's `return') return int. Return types are not affected - not conforming them means invalid code.

Things like this are why I say that named statements and statement expressions are really separate features. You clearly want them to have different behavior in some circumstances.

If I issue a `return` as part of a statement expression, I expect that return to be treated exactly like any other `return` in the code. That is, it will define the function's return type if return type deduction is being used. So if the only `return` call just so happens to be in a statement expression, everything is fine.

But you seem to want named statements to operate differently. Which is fine, but that is also why they are distinct features.

Nicol Bolas> Not to mention, `std::accumulate` itself doesn't have to directly call the functor. It can call a sub-function which calls the functor. Or a sub-function of a sub-function calls the functor. It may even have an internal lambda that does the calling. Who knows?

Specification. I'm okay with any number of nestings in `std::accumulate' as long as returning from the innermost nest allows me to have identical behaviour as the flat version. In other words, a PSE can only be used if you have (valid, specified) assumptions about the internals of the function you call. If you don't, you don't pass a PSE.
It's just a feature.

So what good is a feature that's completely unreliable for any code that you didn't personally write yourself? This feature is looking more and more like P0057 continuations: a limited hack that only works well under certain very specific circumstances. Which only encourages the writing of limited code.

You can't even use it in standard library algorithms, because the standard library makes no guarantees about this sort of thing. And that's a good thing.

It's useful when you're responsible for both the function and the PSE (which is very oft) or when there's a clear specification of what early return does.

The latter pretty much never happens. Nobody goes specifies how many function calls are nested between where a function is taken and where it is called. It's an implementation detail that's none of the outer code's business.

Note that you need to have specifications about how your lambda is called as well.

That is a different thing. With lambdas, the only real question is "will it store the function or not?" And that is usually well-defined by the nature of the function you're using. If the interface is a signal/slot callback, then obviously it will store that function. If it's `std::accumulate`, then obviously it will not store that function.

Some lambdas that try to have mutable state may wonder if the lambda will be copied. But other than that? No.

Contrast this with your limitations here. This is the only C++ feature I know of where the number of function calls in a call graph actually changes its behavior. Indeed, this is exactly why exceptions work the way they do: to be able to give implementations the freedom to be implemented however they want, while still propagating exceptions up to the code that actually knows how to resolve them.

So I see this as a limited and limiting feature. It will encourage people to write their own private implementations of standard library algorithms (potentially inefficient ones), just so that they can have known named statement behavior. It actively discourages splitting functions into logical units. And so forth.

Viacheslav Usov

unread,
Feb 13, 2017, 5:15:42 AM2/13/17
to ISO C++ Standard - Future Proposals, Lorand Szollosi
On Sun, Feb 12, 2017 at 10:47 PM, <szollos...@gmail.com> wrote:
 
Viacheslav Usov> The code calling a functor that is actually a PSE has no idea where the control is transferred to. As I said just above, that code may well expect that the control may not be transferred anywhere at all.

Currently we don't have noexcept for PSEs and it's not clear what it'd mean to have that, given you consider non-local transfer as an exception from this point-of-view.. If you have a function that calls a PSE that calls a PSE, then we have the guarantee that (unless by an actual throw) both the inner and the outer PSE can at most cause the outer function to return. Therefore I think that the function might still be noexcept. This is different from an exception which might propagate outside that function.

The point of noexcept is not that the 'outer' function can be marked as noexcept. The point is that the 'outer' function can call some other functions marked as noexcept, passing those PSE objects to them. And what is the effect of the PSE's return on those nested calls?

Viacheslav Usov> Your example used std::accumulate(). The latter definitely does not know anything about your PSE and the context it was defined in,

It does not know the context, similarly that it does not know the context of a lambda. std::accumulate() accepts a template type. That template type has a (possibly new kind of) template operator(), which is resolved in std:accumulate() automatically by its `return'. Thus it does know about my PSE, at least to the level that it knows that it has to resolve the template return. Naturally this also means that PSEs are inline.

Are you saying that both the PSE and the function, which is called with the PSE, shall be defined in the same translation unit?

Cheers,
V.

szollos...@gmail.com

unread,
Feb 13, 2017, 5:54:56 PM2/13/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
Hi,

Nicol Bolas> Now, this has an obvious hole: if `f` throws, then you leak memory.
Yes, it does. If you throw, it does - there's a workaround for that, as you correctly described. If you call a PSE/NSE, it *might* leak - there's still a workaround for that, as you described. If `f' is a [[noreturn]] function, then we actually leak - and there's no workaround, correct me if I'm wrong.

Nicol Bolas> And the `return` may propagate out of any number of named statement functions. What's the difference?
It can only propagate out of NSEs/PSEs. It cannot propagate out of functions. A code which itself doesn't use exception handling might call into a throwing function and receive an 'alternative return type' in an 'alternative return path'. A code that itself doesn't call PSEs will never receive an alternative return type or get returned to an alternative location even if it passes a PSE. I see this as a difference.

Nicol Bolas> I consider it conceptually rude to be able to impose a `return` on a function. If that function asks to allow you to return for it, that's fine. But it should be something the function explicitly is written to permit. It should never be hidden and it should never be imposed upon you without your will, knowledge, or consent.
I'm okay with that restriction. I originally omitted explicit `take_return` to keep it brief, but there are multiple options here: (where `op` might be a PSE)
//option 1 - explicitly name the continuations that can be taken
template< class InputIt, class T, class BinaryOperation >

T accumulate(InputIt first, InputIt last, T init, BinaryOperation op)
{
   
for (; first != last; ++first)
        init
= op<return>(init, *first);
}


/
/option 2 - `allow taking here`
template< class InputIt, class T, class BinaryOperation >

T accumulate(InputIt first, InputIt last, T init, BinaryOperation op)
{
   
for (; first != last; ++first)
        init
= `op(init, *first)`;
}


//option 3 - allow PSE only if inside a SE
template< class InputIt, class T, class BinaryOperation >

T accumulate(InputIt first, InputIt last, T init, BinaryOperation op)
{
   
for (; first != last; ++first)
        init
= ({ op(init, *first) });
}

//option 4 - convert control flow statement to [[noreturn]] void fn
template< class InputIt, class T, class BinaryOperationWithReturn >

T accumulate(InputIt first, InputIt last, T init, BinaryOperationWithReturn op)
{
   
for (; first != last; ++first)
        init
= op(`return`, init, *first);
}


Depending on how we define them, option 1 and 4 might require separate specialization / overload of the function being implemented. Option 2 is a new char (digraphs :) and, since symmetric, might be problematic to nest. Option 3 is either confusing or logical depending on how you approach it; it definitely needs explanation. Do you find any of these ok, or should we try more alternatives?
Also note that, when I originally started working on this, I didn't even consider allowing to forward a PSE/NSE, as it's not required in most common use cases. Passing it is necessary but - if you find it a good idea - I'm okay with explicitly disabling std::forward<>() on these.

Nicol Bolas> Nobody goes specifies how many function calls are nested between where a function is taken and where it is called.
Nobody used to specify if a variable is going to be reassigned / destructed after a call, as that was the caller's business. Then we started to realize the benefits of move. We don't use move everywhere, only where it's reasonable.
I'm not saying everyone will use it - I don't even want everyone to use it. I see a limited scope for control flow-like statements, of which we tend to have many proposals. NSEs could bring you for..else, for..break, if_optional, accumulate with shortcut, and maybe some others that we don't yet foresee: that's the scope. Instead of separate proposals for these I suggest a language feature that moves these to the library.
Also note, a function that's aware of PSEs and need to delegate internally might as well use delegation to PSEs / NSEs. I think it's reasonable to expect the above to specify if the PSE is called directly.

Viacheslav Usov> The point is that the 'outer' function can call some other functions marked as noexcept, passing those PSE objects to them. And what is the effect of the PSE's return on those nested calls?
IF - and only if - we want to allow forwarding a PSE, then the PSE will return from the innermost function caling it - in this case, the inner function. Hence the outer function should be aware of this. Note that PSEs are not really designed or expected to be passed around this way. If you need an inner function-like object within a function that gets PSE and you want to pass the inner one the PSE, use a PSE/NSE for the inner function-like object as well.

Viacheslav Usov> Are you saying that both the PSE and the function, which is called with the PSE, shall be defined in the same translation unit?
Definitely. PSE body should be visible to the compiler when compiling the function that takes it (to avoid overhead from multiple return addresses); I'm okay if we want to make it visible to the calling function. To me, a PSE's operator() is basically a template.

Thanks,
-lorro

Nicol Bolas

unread,
Feb 13, 2017, 9:10:21 PM2/13/17
to ISO C++ Standard - Future Proposals, szollos...@gmail.com
On Monday, February 13, 2017 at 5:54:56 PM UTC-5, szollos...@gmail.com wrote:
Hi,

Nicol Bolas> Now, this has an obvious hole: if `f` throws, then you leak memory.
Yes, it does. If you throw, it does - there's a workaround for that, as you correctly described. If you call a PSE/NSE, it *might* leak - there's still a workaround for that, as you described.

You mean the workaround I said couldn't be used everywhere?

If `f' is a [[noreturn]] function, then we actually leak - and there's no workaround, correct me if I'm wrong.

No, there is no "actual leak". Returning from a `[[noreturn]]` function is UB. And being undefined means that it is not defined whether it will leak or not.

If the function transfers control elsewhere or terminates or whatever, then even the RAII version would "leak". But nobody cares because the application is being terminated or whatever.

Nicol Bolas> And the `return` may propagate out of any number of named statement functions. What's the difference?
It can only propagate out of NSEs/PSEs. It cannot propagate out of functions.

But since they can nest, the locality of exactly where a "return" codes is not immediately obvious.

Just like exceptions.

I don't know why you're so fundamentally against treating this as a specialized exception. It would solve so many of the problems with your idea: the inability of code between the two points to interfere in the skipping process, the inability to have it `return` through arbitrary numbers of functions, the specific nature of these constructs (are they functions, if not, then what are they, are they objects, etc). The feature becomes far more useful once you use exception resolution as the foundation of it.

If performance is a concern, then for the cases you're most concerned about (everything is inline and visible), I'm sure compilers can be smart enough to optimize it down to triviality. The syntax for such code can also be made more reasonable.

The only real problem with doing it this way is how to deal with the case of someone throwing one return type to a function which returns a different, incompatible one. Since the circumstance is (nominally) a runtime construct, it would need to be a runtime error (like `terminate`).

A code which itself doesn't use exception handling might call into a throwing function and receive an 'alternative return type' in an 'alternative return path'. A code that itself doesn't call PSEs will never receive an alternative return type or get returned to an alternative location even if it passes a PSE. I see this as a difference.

But whether code calls a named statement or not is not necessarily up to that function. So whether a function calls a named statement is not necessarily known to the writer of that function.

Nicol Bolas> I consider it conceptually rude to be able to impose a `return` on a function. If that function asks to allow you to return for it, that's fine. But it should be something the function explicitly is written to permit. It should never be hidden and it should never be imposed upon you without your will, knowledge, or consent.
I'm okay with that restriction. I originally omitted explicit `take_return` to keep it brief, but there are multiple options here: (where `op` might be a PSE)
//option 1 - explicitly name the continuations that can be taken
template< class InputIt, class T, class BinaryOperation >

T accumulate(InputIt first, InputIt last, T init, BinaryOperation op)
{
   
for (; first != last; ++first)
        init
= op<return>(init, *first);
}


/
/option 2 - `allow taking here`
template< class InputIt, class T, class BinaryOperation >

T accumulate(InputIt first, InputIt last, T init, BinaryOperation op)
{
   
for (; first != last; ++first)
        init
= `op(init, *first)`;
}


//option 3 - allow PSE only if inside a SE
template< class InputIt, class T, class BinaryOperation >

T accumulate(InputIt first, InputIt last, T init, BinaryOperation op)
{
   
for (; first != last; ++first)
        init
= ({ op(init, *first) });
}

//option 4 - convert control flow statement to [[noreturn]] void fn
template< class InputIt, class T, class BinaryOperationWithReturn >

T accumulate(InputIt first, InputIt last, T init, BinaryOperationWithReturn op)
{
   
for (; first != last; ++first)
        init
= op(`return`, init, *first);
}


Depending on how we define them, option 1 and 4 might require separate specialization / overload of the function being implemented. Option 2 is a new char (digraphs :) and, since symmetric, might be problematic to nest. Option 3 is either confusing or logical depending on how you approach it; it definitely needs explanation. Do you find any of these ok, or should we try more alternatives?

You're too caught up on the form of this stuff and not enough on the functionality in question. It doesn't matter what syntax you use at this point. The question is what behaviors are you trying to accomplish here?

The minimum necessary behavior is very simple: you cannot call a named statement in the same way as a regular function. The syntax which invokes a function call must not be able to invoke a named statement. And conversely, the syntax which invokes a named statement must not also be able to be used on a callable function.

The details of how that happens are less important than precisely what the behavior will be.

Also note that, when I originally started working on this, I didn't even consider allowing to forward a PSE/NSE, as it's not required in most common use cases. Passing it is necessary but - if you find it a good idea - I'm okay with explicitly disabling std::forward<>() on these.

... what would that mean? I mean, are these things even objects? What you call a "PSE" looks like a lambda, which is an object. So I have the right to get a pointer to it, right? Or pass it via `const&`.

So how exactly do you plan to forbid function A from passing it to function B if you could pass it to function A to begin with?

Nicol Bolas> Nobody goes specifies how many function calls are nested between where a function is taken and where it is called.
Nobody used to specify if a variable is going to be reassigned / destructed after a call, as that was the caller's business. Then we started to realize the benefits of move. We don't use move everywhere, only where it's reasonable.

No, the number of times you copy an object has usually been of some importance to C++ programs. That's why `const&` exists; so that you don't copy the object unless you need to. This was always something that was part of API design; C++11 just added a few extra tricks.

I'm not saying everyone will use it - I don't even want everyone to use it. I see a limited scope for control flow-like statements, of which we tend to have many proposals. NSEs could bring you for..else, for..break, if_optional, accumulate with shortcut, and maybe some others that we don't yet foresee: that's the scope. Instead of separate proposals for these I suggest a language feature that moves these to the library.

So instead of having good, readable, easily digestible syntax like `for/else` or whatever, we have difficult to comprehend syntax that involves creating lambdas and other spurious syntax.

Generality has its place, but not at the cost of readability and such.

Lambdas exist to allow us to make simple functors. That's the purpose of the feature. They are not for manipulating the control flow of a program and they certainly are not for manipulating the control flow of non-local code. My general feeling about lambdas is this: if you're grafting features onto lambdas as a means to solve some deficiency in the language, then the problem either is not worth solving or is best solved with a targeted language feature.

And your `accumulate with shortcut` could be done right now, simply by adding a new algorithm (`accumulate_until` or whatever), where the predicate returns a product type of the new value and a boolean. And you'd need a new algorithm anyway, since you don't want implementations of the regular `std::accumulate` to be limited to the requirements of this feature. So why go through the pain of this for something as simple as that?

3dw...@verizon.net

unread,
Feb 22, 2017, 6:26:09 PM2/22/17
to ISO C++ Standard - Future Proposals


On Monday, February 6, 2017 at 10:46:06 PM UTC-5, Tony V E wrote:
I highly recommend, for almost any proposal, to include "before and after tables" (I think the committee has a some nick name for these), showing what you would currently need to write using C++17 on the left, and how it could be rewritten in C++20 using your proposal on the right.

Just focusing on the aspect of statement expressions now existing in various implementations (no params or templates)
and highlighting the real use case: initialization without scope leak.
I think this has a real chance of being useful and accepted.
Note that C++17 just put in initialization in if and switch for this reason.
In that light, this should work well with that feature and really be a natural extension of it.

OLD:
  {
   
Tp s{};
   
for (int k = 0; k < 3; ++k)
      s
+= v[k];
   
if (s.useable())
      do_a_thing
(s);
 
}



NEW:
  if (foo = ({Tp s{}; for(int k = 0; k < 3; ++k) s += v[k]; s}); foo.useable())
    do_a_thing
(foo);



Decisions and creations of things are limited to the scope of the if.

Also consider selection:
OLD:
  auto Ls_nint = Tp{0};
 
if (x != Tp{0})
 
{
   
auto w = std::log(std::complex<Tp>(x));
    std
::real(polylog_exp_neg_int(n, w));
   
Ls_nint = Tp{0};
 
}



NEW:
  auto Ls_nint = x == Tp{0}
               
? Tp{0}
               
: ({auto w = std::log(std::complex<Tp>(x));
                   std
::real(polylog_exp_neg_int(n, w));});



The latter only defines w if needed. Ls_nint only gets initialized once.

Consider construction:
OLD:
  Matrix(const Matrix& a, const Matrix& b)
  m_A
() // Might be expensive.
 
{
   
if (!valid(a))
     
throw badmatrix("A");
   
else if (!valid(b))
     
throw badmatrix("B");
   
else
   
{
     
auto tmp = a * b;
     
if (!valid(b))
       
throw badmatrix("B");
     
this->m_A = tmp;
   
}
 
}



NEW:
  Matrix(const Matrix& a, const Matrix& b)
 
: m_A(!valid(a) ? throw badmatrix("A")
                 
: !valid(b) ? throw badmatrix("B")
                             
: ({auto tmp(a);
                                  tmp
*= b;
                                  valid
(tmp) ? tmp : throw badmatrix("A*B");})
 
{ }



Maybe that's not as big a big win.
 

Michał Dominiak

unread,
Feb 22, 2017, 6:33:19 PM2/22/17
to ISO C++ Standard - Future Proposals
How is any of those cases not solved naturally by just using an IIFE? There's exactly one problem with IIFEs and that's returning from an outer function, but that has nothing to do with any of the above examples.

--
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/c1b3fd23-9a0b-449f-9cd7-c87f64558e58%40isocpp.org.

Ville Voutilainen

unread,
Feb 22, 2017, 6:34:09 PM2/22/17
to ISO C++ Standard - Future Proposals
On 23 February 2017 at 01:26, <3dw...@verizon.net> wrote:


On Monday, February 6, 2017 at 10:46:06 PM UTC-5, Tony V E wrote:
I highly recommend, for almost any proposal, to include "before and after tables" (I think the committee has a some nick name for these), showing what you would currently need to write using C++17 on the left, and how it could be rewritten in C++20 using your proposal on the right.

Just focusing on the aspect of statement expressions now existing in various implementations (no params or templates)
and highlighting the real use case: initialization without scope leak.
I think this has a real chance of being useful and accepted.
Note that C++17 just put in initialization in if and switch for this reason.
In that light, this should work well with that feature and really be a natural extension of it.

OLD:
  {
   
Tp s{};
   
for (int k = 0; k < 3; ++k)
      s
+= v[k];
   
if (s.useable())
      do_a_thing
(s);
 
}



NEW:
  if (foo = ({Tp s{}; for(int k = 0; k < 3; ++k) s += v[k]; s}); foo.useable())
    do_a_thing
(foo);




Why isn't OLD then

if (foo = [] {Tp s{}; for(int k = 0; k < 3; ++k) s += v[k]; return s;}(); foo.useable())
    do_a_thing
(foo);

3dw...@verizon.net

unread,
Feb 23, 2017, 11:36:04 AM2/23/17
to ISO C++ Standard - Future Proposals

You're right.  And with a special keyword like `xxx_yield` or `vomit` they would be the same length.
 
Maybe all my examples could be done this way.

Question: what context does a lambda have in a ctor init list?
Ctor args, globals, ...?

Reply all
Reply to author
Forward
0 new messages