Skip to content

Instantly share code, notes, and snippets.

@Anton3
Last active July 10, 2020 23:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Anton3/94142f9783bf4c938aa8dcda2c561305 to your computer and use it in GitHub Desktop.
Save Anton3/94142f9783bf4c938aa8dcda2c561305 to your computer and use it in GitHub Desktop.

A summary of previous discussions on P2025

Objections to the proposal

Disclaimer: I’m undoubtedly biased. You can find the original opinions in the thread “P2025R1: issues fixed?”, 13/05/2020 to 20/05/2020.

1. The proposed language rules are too complex.

The wording consists of two major parts:

  1. Formalizing what happens during copy elision (#wording-behavior). The current equivalent is obscure to the point where I’d almost consider it a defect — the proposal replaces that with a precise description. Normal users don’t necessarily need to learn all the details of that.

  2. Describing when copy elision is allowed or required (#wording-definitions). That’s the part that developers will most certainly need to understand to use the feature efficiently. There are nuances, but the gist of it is “Copy elision is guaranteed for return x; if every return ‘seen’ by x is return x;” (see #proposal).

2. The proposed rules are intrusive. Almost every current program will contain cases where “guaranteed NRVO” applies. For a C++ developer to be efficient, they’ll need to be taught the intricate proposed rules.

Currently, a lot of C++ developers don’t really care if copy elision applies — a move is enough for them. Those developers won’t benefit from the proposed changes and won’t have to learn anything. They might still get a small free performance boost.

Those developers who do care (e.g. who want to return non-movable types or objects for which a move is undesirable, see #motivation) will stop using draconian workarounds and start benefiting from the new feature. Note that those developers currently have to learn all the intricacies of copy elision — and additionally implementation-specific details for various compilers, and also the workarounds. If the proposal is accepted, returning non-movable types will turn from an expert-only topic into a mostly a beginner topic (in complex cases one might still want to reach for a reference). All in all, I’d argue that “guaranteed NRVO” will actually reduce the complexity those developers deal with.

3. Whether a variable is a “return variable” is context-dependent. It will be difficult to detect one at a glance.

Whether a variable is a return variable is not important in itself. The question which will arise in practice is whether a move will happen at return x;.

As noted above, the check of whether all the return statements are return x; will cover most use cases — although context-dependent, the check is purely syntactic and can be performed fast and easily.

Then there are the nuances:

  • volatile variables inhibit “guaranteed NRVO”
  • function parameters are out of luck
  • trivially copyable types are essentially denied “guaranteed NRVO”

(This is the complete list of nuances blocking “guaranteed NRVO”)

4. The trivially-copyable nuance will become a huge foot-gun once people start relying on “guaranteed NRVO”.

It is. A little control-flow change can break existing code relying on “guaranteed NRVO”. There is one way to fight it: whenever we create a variable, save a pointer to it and return it, we must ensure that the variable’s type is non-copyable, non-movable. If it is movable, then we can wrap it in a #pin.

5. I still want an explicit syntax for return variables instead of your conjured-up ad-hoc rules, for all of the reasons above. Also, I don’t use non-movable types, so stop bloating the language.

It may be a case of wanting a screaming syntax for a scary new feature. The feature can be viewed in combination with [class.copy.elision]/3: in return x;, if bla-bla-bla, then no copy or move is performed; else if bla-bla-bla, then a move is performed; else a copy is performed. See also: #explicit-mark.

Wording issues

1. The proposal leads to invalidation of optimizations: every extern variable of the returned type could alias the return variable.

Status: PARTIALLY-FIXED

The case is described in #invalidation and addressed in wording in #wording-behavior (section 5). No optimizations should be invalidated in this case.

However, the proposed wording accidentally disallows using this pointer in the constructor of the object, see #wording-behavior (section 4):

[…] if the value of the object of the return variable or any of its subobjects is accessed through a glvalue that is not obtained, directly or indirectly, from the variable […]

It should be “from this pointer during the initialization of the variable or from the variable name”.

2. The proposal does not clarify the storage duration of the return variable. It seems like the object has automatic storage, but its storage depends on the context in which the function is called.

Status: NEEDS-REVISITING

The case is addressed in #wording-behavior (sections 1, 2, 3). If the scope of the variable is exited by returning it, the object has whatever storage duration the function result object has. If the scope is exited by other means, the object has automatic storage duration and needs to be destroyed.

Additionally, in any case, even if the variable is returned, see #wording-behavior (section 2):

[…] Except for that, until the control is transferred out of the function, the variable shall be treated as a local variable with automatic storage duration […]

Issue: do we need this remark at all? Are there any benefits to being treated as having automatic storage duration? The proposed wording doesn't require that to destroy the object correctly.

3. What happens if the return variable ends up not being returned, the object is destroyed, then another return variable is constructed? Is the result object constructed and destroyed multiple times?

Status: FIXED

The return variable’s object is only considered the result object if it ends up being returned, see #wording-behavior (sections 1, 2, 3), and #issue-2dd63f8d. Also: #ex-10 and #ex-11.

There might be a better way to work around the issue, but the proposed way does work.

4. If one return variable’s object is replaced by another one, do pointers to the old one remain valid?

Status: PARTIALLY-FIXED

According to [basic.life]/8, they should, unless the old variable is declared const. The storage is the same: #wording-behavior (section 1). See also: #ex-20.

However, the return variable’s object may not complete if it ends up being returned. We could solve the issue by modifying the proposed wording to say that the object is treated as a complete object inside the function. Or we could instead add to [basic.life]/(8.5): “or o1 and o2 are denoted by return variables in the same immediately-enclosing function or lambda-expression”.

5. What about trivially-copyables? They must be allowed to be copied, otherwise we risk ABI breakage and inability to return by register.

Status: FIXED

A copy of such a return variable is still allowed: #trivial-temporaries. Also, in wording: #wording-behavior (section 2).

6. The statement regarding whether a statement returns a variable may need fixing for lambdas.

Status: FIXED

See #wording-definitions (section 1) and #ex-12.

7. [basic.stc.auto] needs an update, because return variables don’t necessarily have automatic storage duration.

Status: NEEDS-REVISITING

We should modify it to reflect that the storage duration of a block-scope return variable depends on how the function call expression is used.

8. [basic.life]/10 needs an update, because a return variable can occupy the same storage as a previous const return variable.

Status: OPEN

Apparently, yes, and updating [basic.life]/10 is the best solution.

9. Stack unwinding is not supposed to destroy objects of static storage duration.

Status: FIXED

If the control flow escapes the function by means other than returning the return variable, then it's said that the object is not (and never have been) the result object (which could have any storage duration), but is a normal local variable with automatic storage duration.

10. The paper uses the terminology around RVO and “guaranteed copy elision” too frivolously.

Status: FIXED

#introduction now explains the terms used in the paper.

11. More examples needed.

Status: FIXED

See #examples.

12. It would be useful to have an implementation of the proposal.

Status: FIXED

See #implementation.

13. Is it necessary to leave non-guaranteed [class.copy.elision]/(1.1) at all? No implementation performs more copy elision than the proposal requires.

Status: NOT-AN-ISSUE

The paper explores one such quality-of-implementation case in #potential-elision. Plus, P0889 proposes to allow more non-guaranteed copy elision that is only possible if the function is inlined.

14. Currently, NRVO can be performed for non-class types that don't fit into registers. Can we allow copy elision for those cases?

Status: NEEDS-REVISITING

The paper now does allow copy elision for non-class types. Like for other trivially-copyables, a copy can be made, but when it doesn’t, “guaranteed NRVO” applies. See #non-class and #wording-non-class.

However, if the implementation is honest in this regard and says that a trivial copy is not made, then it complicates the #escape-analysis. Furthermore, it may break existing code that assumes the address of x in T x = foo(); (where T is a non-class type) can't have already escaped. Do we need to standardize something optional that implementations will likely ignore?

15. It may be difficult to implement accounting for discarded return statements (constexpr if), because the “NRVO check” is performed syntactically during the initial parse of a template.

Status: NEEDS-REVISITING

#ex-18 would be difficult to get working. Richard Smith reported it would be difficult to implement in Clang. Sean Baxter found that this single part would take too much effort to implement in the Circle compiler.

16. What if an exception is thrown during the destruction of locals?

Status: FIXED

See #wording-behavior (section 3) and #implementation-exceptions.

17. What if the result object is a constexpr global and is put into read-only memory?

Status: NOT-AN-ISSUE

Just like an object under construction, it can be modified at compilation time, then baked into read-only memory after the function returns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment