Live data from Hacker News

Move in C++ without a std:move

andreasfertig.com

31–40 of 75 posts

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

#31

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…

Register allocation is usually on a far "lower" codegen level as is often DCE, they should be possible to compute/run on a SSA node level or similar long after destruction sequences are applied.

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.

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

#32
post #8
post #3

Earlier quoted context omitted.

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

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

This is an easy mistake to make but it's not what happened

Programmers already knew (~20 years ago) when the C++ move feature was designed that what people want is the destructive move assignment semantic, the thing Rust has today. Other languages did have that. But C++ 98 already existed and WG21 already did not want to make it difficult to take your crusty 10+ year old C++ codebase, slap a sticker on it and say this is "Modern C++"

The C++ "move" proposal is slightly sneaky, it admits that what they're proposing is not the destructive move (again, people know they want this) but it gives the impression that if they really want destructive move they can add it later, without revealing what's really going on underneath.

In fact C++ move is roughly what Rust would call core::mem::take, we move something in the usual way (a destructive move) but then we replace it with some value of the same type, in the case of core::mem::take it's Default::default()

To enable this, C++ is full of types which look superficially familiar to a Rust programmer but have a weird "empty" state to provide that default value where none would make sense. For example std::unique_ptr looks like it's Box but it's not, it's actually Option>, even newer types often do this but they might be more embarrassed about it.

[Edited to clarify timeline]

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

#33
post #10

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?

The simple cases are simple. However the complex cases get hard. 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…

That one is not too hard either. `one` simply can't be NRVO'd as there is a runtime path in scope of it that doesn't return it. `two` can be (unless you hide some other return within ...).

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

#34

Earlier quoted context omitted.

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)…

They probably _were_, since lower level code representations often has little notion of complex types and their semantics they could be kept clean and focused on machine code, however type inference in a language like C++ complicates such matters immensly since an assignment can be both a register move and a function call.

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.

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

#35
- stupid question, since we are on the topic of c++, i finished reading through learncpp.com

- how do I take this from 0 to the guy that builds a play station 3 emulator?

- like seriously what kind of step by step projects or learning experiences in increasing order of difficulty do you recommend to get really really good at c++

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

#36

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…

>rewriting the function signature to return void and take an extra pointer parameter, which is written to before returning

This is completely unrelated to RVO. Every non-trivial class is returned via pointers to caller-allocated storage under the Itanium ABI. Period.

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

#37
post #19

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, it works exactly like this, this is a demo on godbolt [0]. rdi stores the pointer in both cases, makeS1() uses RVO, makeS2() takes it explicitly and constructs with placement new. 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…

No, all you're showing in that example is that a pointer is passed as part of the ABI. You're not showing that RVO relates to that in any way whatsoever. If you write the same function in a manner that (N)RVO can't kick in, does the pointer no longer get passed?

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.

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

#38

Earlier quoted context omitted.

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)…

They probably _were_, since lower level code representations often has little notion of complex types and their semantics they could be kept clean and focused on machine code, however type inference in a language like C++ complicates such matters immensly since an assignment can be both a register move and a function call. Template resolution solved that in the past, but C++ today allows auto in so many places that I…

> 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).

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.

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

#39
Kudos to the author for explaining these concepts, but I personally find this to be a poor use of my time and mental capacity.

I dabbled in C++ programming before Rust 1.0 was released (year 2015). I have spent some time understanding things like RVO, std::move(), rvalue references, T&&, move constructors, and so on. This was the main tutorial that I read a decade ago: https://web.archive.org/web/20240108142848/http://www.thbeck...

I began programming in Rust in 2017, and it is such a breath of fresh air. It has all the power of C++ but shed all the unnecessary baggage (e.g. confusing features, duplicate features).

In relation to this article, I like Rust's clear and simple semantics about moving objects. If x and y are variables of type T which implements the Copy trait, then `x = y;` performs a simple bitwise copy. Otherwise, `x = y;` moves the object `y` to `x` and `y` is no longer allowed to be accessed ("moved away"). Whereas in C++, the article states how copy elision behavior has changed over the years:

> You get guaranteed copy elision since C++17 in the following case [implying it wasn't guaranteed before C++17]

> Then you have named return value optimization (NRVO): [...] The latter is not subject to guaranteed copy elision. You probably don't pay for a copy or move there as well.

> While this code compiles, you will get a copy construction of the return value before C++20.

> Once you switch your compiler to C++23 mode, both cases do an implicit move. No std::move required.

This is why I prefer to deal with Rust instead of C++. And this is just my commentary on specifically the behavior of moves in C++. When I pile on other issues - like copious undefined behavior everywhere (e.g. signed integer overflow, array out-of-bounds accesses), too many footguns that result in bugs and security vulnerabilities - I developed an extreme distaste for programming in C and C++.

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

#40

- stupid question, since we are on the topic of c++, i finished reading through learncpp.com - how do I take this from 0 to the guy that builds a play station 3 emulator? - like seriously what kind of step by step projects or learning experiences in increasing order of difficulty do you recommend to get really really good at c++

Getting very good at C++ takes more than a decade. There are no shortcuts. You will have to start by getting in the trenches and write some code.
Post reply on HN