mytype foo() {
mytype one;
...
if(something) {
mytype two;
...
return two;
}
return one;
}
Is going to be much harder because you don't know are compile time which is returned and so cannot construct the one you return in the correct place. That is just off the top of my head, I'm not a compiler writer, I'm sure they have figured out the simple versions of the above, but you can start to see the complex versions that they can't.In RVO there is no problem because you know what object is the one you need to put there.
In NRVO there is a problem because you might have one of multiple objects being returned and you need to know which one to construct at the call site; it can't be all of them on top of each other. But you don't necessarily know at the time of construction whether that object will be the one that is actually returned. Doing so requires imperfect code analysis so the standard would need to define the complicated analyses to perform.
NRVO requires the compiler to analyze the flow, like if 2 different variables/constructions can lead to the return (what one do we take, or can we do either later?).
Also, with RVO it's easy to detect and elide destruction calling for things going out of scope whilst NRVO would require more careful management of destruction order,etc.
Basically, NRVO touches a lot of things in "inconventient" places that can easily require reworking internal compiler structures to track destinations whilst RVO was probably far easier to just "hack in".
Maybe it's the guaranteed elision that makes it a problem, like you can't fail the analysis, but then maybe you go the rust route - fail to compile and urge the programmer to rewrite their code so it accepts it.
Make it opt in with [[must_elide]] so old code still works I guess.
The frontend is extremely reluctant to do anything like flow analysis, in large part because the frontend doesn't even really have any code to do the analysis on, just the AST. More people (including far too many on the committee itself) need to understand the separation between the different parts of the compiler, and what each part can and cannot do effectively.
Template resolution solved that in the past, but C++ today allows auto in so many places that I'm uncertain that it can be done without some flow based support (if constexpr comes to mind).
I mentioned ClangIR(MLIR) in the sibling comment here, feels like it was built for stuff like this.
C++ requires determining the type of every expression immediately. Even auto doesn't change that: it determines the type of the variable based on the initializer, literally following the same rules as template argument deduction (there's a little bit of patching to tweak the exact expression being used for deduction, but https://eel.is/c++draft/dcl.type.auto.deduct#3 is the core rule here).
ClangIR doesn't necessarily help here, because while it does give a more abstract C abstract machine IR semantics, it's still downstream of things like NRVO decision points--it's still fundamentally past the codegen-the-AST barrier.
This concern is unfounded. The auto keyword in C++ acts as mere syntactic sugar. It works only when the compiler is able to tell exactly what's the type by evaluating the expression.
The auto keyword is also considered a code smell for the same reason: just because the compiler can tell exactly what the type is expected to be, that does not mean the developer can. Therefore it makes the code harder to reason about.
This really depends though. In many cases even the programmer can tell the type of auto because its on the very same line and not using auto would mean needlessly repeating it.
In other cases (e.g. iterators) the programmer also doesn't need to care about the concrete type.
I'm also fine with auto if it just repeats information, especially so if the concrete type takes half of your line length budget (yes it's an iterator over that container containing...).
Iterators is the common example: the type name is always long, and nearly always used in a context where it is obvious. Even if you do care about the type, looking up the iterator is wrong - you always first look up the base type (ie std:vector) first to understand the iterator, only rarely do you need to dig into the iterator once you understand the base type.
Generic code (template and non-template) are the other - they type could be anything, so trying to specify more detail isn't going to gain you anything.
Now, there is far more "language level" flow analysis today apart from this as required by allowing auto type inference in more places (and things relaxed in relation to that). Reading up it seems to be suitably done in Clang on the ClangIR(MLIR extension) level, something that sits between AST and the LLVM IR.
Regardless of how it's implemented, I'm pretty sure that NRVO carried a fair bit more complexity requirements compared to RVO depending on how prepared the corebases for different compilers were to handle it.
A "perfectly predictable" algorithm was proposed in Anton Zhilin's P2025, back in the year 2021: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p20... but unfortunately it had some subtle corner-case problems (which I do not remember), so it was sent back for revision, and never returned with a fix. (Maybe because a fix wasn't possible; again I don't remember what the deal was exactly.)
The Right Path Forward would be for MSVC, GCC, and Clang all to try implementing P2025's algorithm in their front ends. Either something concrete breaks (reminding me what the problem was), or else all three mainstream compilers gain predictable NRVO and then we can "standardize existing practice." But the Right Path Forward requires tedious work by at least three people, which is hard.
I'm not too confident on that last part, because such an implementation would mess with semantics in case of an exception, so anyone feel free to correct me on that.
This sounds wrong, are you sure? Would you mind demonstrating with an example on godbolt? Whether NRVO applies or not, the ABI should be the same, AFAIK.
https://godbolt.org/z/9WvnEvEYh Note how `std::unique_ptr<int>` effectively passed as a `int**`; and that the by-value unique_ptr is not destroyed at the end of the function -- destroying parameters is instead the caller's job (and commonly only happens at the end of the full expression containing the call -- though this choice is implementation-defined). But that can only work if the caller can see the updated value of the parameter (to avoid double-free for `clear`) -> thus the need to pass the parameter by hidden pointer.
I will say before testing this i didn't realize the RVO calling convention was to return the pointer you pass in, but apparently so. If makeS2() returned void, it's just a tail call to the constructor, but makeS1() has to spill rbx and use it to save the pointer.
The reason this should sound dubious is that you're suggesting the caller needs to know the callee's body in order to know how to call it, but it should be possible for the two to be compiled entirely independently, and in fact mutual recursions should be fine too. After all, the callee knows where the return value has to land either way, and the caller similarly knows where to expect it, regardless of when/how the object is constructed or destroyed.
NRVO does not affect the ABI of the function. It cannot affect the ABI, for whether or not it kicks in depends on the body of the function, and affecting the ABI would make it impossible to use it if only the declaration appears in a header.
The correct explanation is this:
In C++, classes with nontrivial destructors or copy/move constructors are considered nontrivial for the purposes of calls and are passed via pointers rather than via value. By passing via pointer, the class has a stable address and thus 'this' pointer. Returning such a class means the caller allocates the storage for the class on the stack before calling the function, and passes the pointer to that storage to the function as an extra parameter. This is based solely on the definition of the class itself; this happens whether or not NRVO kicks in.
Usually, when you declare a variable, the abstract machine of C++ requires you to construct a new object and call the copy/move constructors or assignment operators and the destructors at various times as appropriate. With nontrivial versions of these special functions, it is possible to observe whether or not they were called (these things still happen with trivial classes, but it's not so easy to observe). Returning a value requires constructing the storage space for that object--with all the attendant abstract machinery that involves.
What NRVO does is to say that, under certain conditions, rather than constructing storage space for a given variable that is normally required, the storage space that is allocated for the return value by the ABI is used instead. In essence, you are promoting a given variable to the return value hence the name 'Named Return Value Optimization'. What makes this annoying to implement is that you have to track at the AST level, before doing any code generation at all, whether or not a given variable is eligible for NRVO, and then use that information to control the code generation for allocating storage space.
Despite its name, NRVO is not actually an 'optimization' in the compiler. The optimizer plays no role in it, since the optimizer is fulfilling the requirements of the abstract machine. Instead, it is a set of conditions that allows the frontend to omit calls to copy constructors, etc. under specific circumstances.
That's semantics. The way the compiler is structured and in which component the transformation is implemented has no bearing on whether something is an optimization.
This is completely unrelated to RVO. Every non-trivial class is returned via pointers to caller-allocated storage under the Itanium ABI. Period.
I recall reading that at a high level RVO is implemented by treating the return value as an external object. In simple terms (simplistic terms) RVO then works by
- first instantiating the return variable,
- passing the var by reference to the function,
- and then use return value to actually initialize the variable passed by reference.
The moment there's some funny logic on what to write to that output value, the problem gets far more complex.