C++ Logo

std-proposals

Advanced search

Re: [std-proposals] std::elide

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Thu, 23 May 2024 09:31:25 -0400
On Thu, May 23, 2024 at 4:46 AM Lénárd Szolnoki <cpp_at_[hidden]>
wrote:

> On 20/05/2024 15:15, Arthur O'Dwyer via Std-Proposals wrote:
> >
> > ISTR that there was a push to get Clang to implement the P2025 algorithm
> > a few years ago; maybe Matheus Izvekov was involved? I don't know to
> > what extent it succeeded, but it actually looks really good in Clang
> trunk:
> > https://godbolt.org/z/n8eh5xdTT <https://godbolt.org/z/n8eh5xdTT>
>

Yep, this was: https://reviews.llvm.org/D119792
And it looks like it was not Matheus Izvekov but rather @Izaron (Evgeny
Shulgin) with the patch. I got the "Iz" right. ;)

> Lénárd Szolnoki wrote:
> > > Add a [[clang::nrvo]] attribute on the variable you want to force
> nrvo on
> >
> > I have a partial implementation of [[clang::nrvo]] in my Clang fork. It
> > expresses the programmer's intent and verifies (in the code generator)
> > that RVO is actually performed. [...]
> > [[clang::nrvo]] return m;
>
> Interesting. I would much rather see the annotation on the variable
> declaration rather than the return statement. My main concern is
> diagnostics, and more indirection in the non-local reasoning required
> for the feature to work.
>

I've considered both ways (out loud, on this mailing list).
Suppose that we want a given return statement to be "NRVOed without move,"
i.e. make this attribute a solution to the "locked mutex" problem. Then, at
the point of variable declaration *and almost everywhere in the function*,
the variable `m` will behave like a normal C++23 variable. The only place
it will suddenly behave differently — by not requiring a move-ctor where
the language says it ought to have one — is exactly at the `return` or
`throw` statement. And if there are two `return m` statements, then *each*
of them will behave interestingly. The interesting/different/novel/relaxed
behavior (from the core language's point of view) happens at the `return`
or `throw`, not at the point of declaration.
>From the code generator's point of view, yes, the declaration is special
because the variable has to be allocated into the return slot. But from the
programmer's and language's point of view there's nothing special about
that; we do things like that all the time. It's the "return without moving"
that is novel.

Another reason to put it on the `return` or `throw`, IMHO, is because it
makes it clear what behavior you expect. The programmer might not realize
that in e.g.
    [[fantasy::nrvo]] Widget w;
    if (cond) return w;
    throw w;
he's implicitly asking for *both* the return and the throw to be NRVO'ed.
There might even be cases in which the same source-level variable could be
NRVO'ed along one path and moved along another path, due to
splitting/hoisting optimizations. But if you say "*This* control-flow
construct must be NRVO'ed," that's very clear what's going on.

Another intuition: You know some folks hate attributes. ;) So if this were
ever to be tackled by WG21 (which it won't be), they'd probably try to make
it a new keyword, such as
    moveless_return w;
    moveless_throw w;
with all the same semantics as the existing `return` and `throw` keywords
except that it wouldn't require the type to have an accessible move
constructor and destructor.
Then the attribute'd `return` statement stands in for that new
`moveless_return` keyword, fitting into the same place in the program but
using today's syntax.


Immovable f(bool cond) {
> Immovable a("foo");
> Immovable b("bar");
> if (cond) {
> [[clang::nrvo]] return a;
> }
> [[clang::nrvo]] return b; // ERROR:
> /* an other local variable is selected for NRVO by a previous return
> statement. (other return statement is on line x, other object
> declared on line y) */
> }
>
> vs.
>
> Immovable f(bool cond) {
> [[nrvo]] Immovable a("foo");
> [[nrvo]] Immovable b("bar"); // ERROR:
> /* can't have two [[nrvo]] objects in the same scope.
> (other object declared on line x) */
> if (cond) {
> return a;
> }
> return b;
> }
>

Well, it's not obvious to me that you 100% *can't* have two [[nrvo]]
objects in the same scope. You might be right in this case, but I'm not
sure such a diagnostic would be correct in general. Even in this case, a
sufficiently smart compiler could see that `cond` never escapes, and hoist
the test, producing the equivalent of...

Immovable f(bool cond) {
   if (cond) {
     Immovable a("foo");
     Immovable b("bar");
     return a;
   } else {
     Immovable a("foo");
     Immovable b("bar");
     return b;
   }
}

I claim this is functionally equivalent to the original code. So, with my
[[clang::nrvo]] on both return statements, the programmer is basically
telling the compiler, "You must do this hoisting optimization, or something
equivalent to it; and if you can't figure out how to do that, then I want
you to give me a diagnostic."

https://godbolt.org/z/cEeYc9sW7

Disclaimer: I didn't actually check your fork, so I don't know if these
> error messages would be representative for that. Please correct me if
> those are better.
>

Currently, my diagnostic (which can be elevated with `-Werror=nrvo` for
those who really care) comes from the code generator, so it really has no
idea *why* NRVO didn't happen — just *that* it didn't happen. So your
diagnostics are vastly more "helpful" than what I actually implemented, in
both cases. :)

–Arthur

Received on 2024-05-23 13:31:42