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.
From: szollos...@gmail.com Sent: Monday, February 6, 2017 8:57 PM To: ISO C++ Standard - Future Proposals Reply To: std-pr...@isocpp.org Subject: [std-proposals] Statement expressions proposal |
I may be misunderstanding something, but how is the proposed ({ statements; expr; }) different from [&]{ statements; return expr; }()? Is there more to it?
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 expressionat 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:
#define return_if_false(a) ({ auto tmp = (a); if (!tmp) return std::move(tmp); std::move(tmp); })
#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
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.
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.
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;
});
...?
>> 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.
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?
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; }(); }
... 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;
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.
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.
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.
auto i = [&] {
//several
//lines
//of
//code
};
auto i = [&] {
//several
//lines
//of
//code
}();
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.
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.
Consider something as simple as this:
named_statement stmt() = ({produce foo(thingy);});
So... what does the compiler do with `stmt`? What does that mean?
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); })
int foo(int);
auto paramstmt = [&](auto&& thingy)
/* -> decltype(foo(thingy)) */
// if needed
({ produce foo(thingy); })
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.
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.
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.
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_);
}();
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); })
...
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'):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.
- 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.
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.
`return/break/continue` into their calling contexts. Presumably through multiple layers of such functions.
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.
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
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.
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;
}));
}
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> 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.
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.
//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);
}
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.
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.
{
Tp s{};
for (int k = 0; k < 3; ++k)
s += v[k];
if (s.useable())
do_a_thing(s);
}
if (foo = ({Tp s{}; for(int k = 0; k < 3; ++k) s += v[k]; s}); foo.useable())
do_a_thing(foo);
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};
}
auto Ls_nint = x == Tp{0}
? Tp{0}
: ({auto w = std::log(std::complex<Tp>(x));
std::real(polylog_exp_neg_int(n, w));});
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;
}
}
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");})
{ }
--
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.
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);
if (foo = [] {Tp s{}; for(int k = 0; k < 3; ++k) s += v[k]; return s;}(); foo.useable())
do_a_thing(foo);