Live data from Hacker News

Move in C++ without a std:move

andreasfertig.com

21–30 of 75 posts

Re: Move in C++ without a std:move

#21

Earlier quoted context omitted.

> N/RVO works by (at the machine language level, of course) rewriting the function signature to return void and take an extra pointer parameter 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.

Yes, of that I'm sure. This optimization is only possible if the compiler has control of both sides of a call. If the function may be callable from other translation units or modules I imagine it generates a thin wrapper that's externally callable.

The optimization is often possible even if the computer does not see the call, because most (all?) ABIs have always required hidden pointer parameters for class types with non-trivial destructors.

https://godbolt.org/z/9WvnEvEYh Note how `std::unique_ptr` 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.

Re: Move in C++ without a std:move

#23

What is that makes NVRO so much more difficult to implement? Why couldn't they mandate that just like RVO? Do compilers literally just special case a simple return statement of a direct construction or something?

N/RVO works by (at the machine language level, of course) rewriting the function signature to return void and take an extra pointer parameter, which is written to before returning. If you're returning a newly-constructed object, the compiler can rewrite that into calling the constructor on the pointer, but if you're returning a named object, the class may have a non-trivial destructor that needs to run after the move…

I'm sorry, this comment is completely wrong.

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.

Re: Move in C++ without a std:move

#24
post #8

Earlier quoted context omitted.

This is one case where Rust benefited from C++’s experience — move by default with opt-in clone/copy is IMO the better setup.

And the most important idea: destructive moves. Since C++ doesn't track lifetimes it has to leave the object in a "valid state" after a move and the destructor still runs which has to have a check if it should do something or not.

BTW, Rust's lifetime annotations for borrowed references are a mostly orthogonal feature.

Liveness of objects for move/drop semantics is tracked differently, without any syntax and with implicit runtime drop flags where necessary.

C++ could probably add the same deinitialized/moved-from state tracking (with an opt-in for back compat sake) purely to avoid dtor bloat, without having to add safety of borrow checking.

Re: Move in C++ without a std:move

#26
post #11
post #3

Earlier quoted context omitted.

Not sure what you mean, but std::move is one of the greatest tools in C++

std::move is a great tool when used correctly. However used incorrectly it makes code worse: more verbose and less performant. Since I have no idea how you are using it I can't comment on your experience. My experience is people (including me!) get it wrong fairly often. Fortunately tools can detect a lot of cases where you get it wrong.

My little trick is to think of them as "oh, I accidentally made an lvalue from an actual rvalue here because a name was introduced, so I need to cast (i.e move() or forward()) back to an rvalue again", that's why i have them as macros: MOVE_CAST and FORWARD_CAST defined as static_cast (also avoids blowing up compile times). I never think in terms of "moving this object".

Re: Move in C++ without a std:move

#27
post #24

Earlier quoted context omitted.

And the most important idea: destructive moves. Since C++ doesn't track lifetimes it has to leave the object in a "valid state" after a move and the destructor still runs which has to have a check if it should do something or not.

BTW, Rust's lifetime annotations for borrowed references are a mostly orthogonal feature. Liveness of objects for move/drop semantics is tracked differently, without any syntax and with implicit runtime drop flags where necessary. C++ could probably add the same deinitialized/moved-from state tracking (with an opt-in for back compat sake) purely to avoid dtor bloat, without having to add safety of borrow checking.

> C++ could probably add the same deinitialized/moved-from state (with an opt-in for back compat sake) purely to avoid dtor bloat, without having to add safety of borrow checking.

There is a lot of talk in the C++ committee about this. The details are complex in some obscure cases.

Re: Move in C++ without a std:move

#30

Earlier quoted context omitted.

RVO is easy to detect since it happens only in expressions in return-statements. 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. Ba…

I figured any half decent compiler already do plenty of flow and liveness analysis on everything for register allocation, dead code elimination and what not. 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 sti…

Despite its name, NRVO isn't an optimization performed by the optimizer, it's something done by the frontend of the compiler before it generates the code for the optimizer to run on.

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.

Post reply on HN