Live data from Hacker News

The repercussions of missing an Ampersand in C++ and Rust

nablag.com

31–40 of 128 posts

Re: The repercussions of missing an Ampersand in C++ and Rust

#31
post #4

Rust's behavior of moving without leaving a moved-out shell behind also simplifies the implementation of the type itself, because its dtor doesn't have to handle the special case of a moved-out shell, and the type doesn't even need to be able to represent a moved-out shell. For example, a moved-out-from tree in C++ could represent this by having its inner root pointer be nullptr, and then its dtor would have to check…

This was one of the most unsatisfying things about learning C++ move semantics. They only kinda move the thing, leaving this shell behind is a nightmare.

C++ doesn't have ownership baked into the language like Rust does, and "move semantics" is all about ownership (under the hood it's just a plain old shallow copy both in C++ and Rust). Making the moved from object inaccessible like in Rust would have required static ownership tracking which I guess the C++ committee was afraid to commit to (and once you have that, you're basically halfway to Rust, including the downside of a more restrictive programming model).

Re: The repercussions of missing an Ampersand in C++ and Rust

#32
post #4

Rust's behavior of moving without leaving a moved-out shell behind also simplifies the implementation of the type itself, because its dtor doesn't have to handle the special case of a moved-out shell, and the type doesn't even need to be able to represent a moved-out shell. For example, a moved-out-from tree in C++ could represent this by having its inner root pointer be nullptr, and then its dtor would have to check…

> For example, a moved-out-from tree in C++ could represent this by having its inner root pointer be nullptr, and then its dtor would have to check for the root being nullptr,

delete null is fine in C++ [1], so, assuming root either is a C++ object or a C type without members that point to data that also must be freed, its destructor can do delete root. And those assumptions would hold in ‘normal’ C++ code.

[1] https://en.cppreference.com/w/cpp/language/delete.html: “If ptr is a null pointer value, no destructors are called, and the deallocation function may or may not be called (it's unspecified), but the default deallocation functions are guaranteed to do nothing when passed a null pointer.”

Re: The repercussions of missing an Ampersand in C++ and Rust

#33

With Rust executing a function for either case deploys the “optimal” version (reference or move) by default, moreover, the compiler (not the linter) will point out the any improper “use after moves”. struct Data { // Vec cannot implement "Copy" type data: Vec , } // Equivalent to "passing by const-ref" in C++ fn BusinessLogic(d :&Data) { d.DoThing(); } // Equivalent to "move" in C++ fn FactoryFunction(d: Data) -> Own…

Can't run Godbolt on my phone for some reason, but in this case I expect compiler to ignore wrapper types and just pass Vec around.

If you have

    Vec

    // newtype struct 
    struct Data{ data: Vec }

    // newtype enum in rust
    // Possibly but not 100% sure 
    // enum OneVar { Data(Vec) }
From my experiments with newtype pattern, operations implemented on data and newtype struct yielded same assembly. To be fair in my case it wasn't a Vec but a [u8; 64] and a u32.

Re: The repercussions of missing an Ampersand in C++ and Rust

#34
post #17

Earlier quoted context omitted.

If the implications of a one char diff are this egregious that they’re considered obvious, maybe it should take less cognitive effort to spot this? CI and tooling are great, but would be far less necessary if it was more difficult to make this mistake in the first place.

What do you suggest? Some kind of std::const_reference ? Clang-tidy is enough in addition to the reviews.

The person is arguing that it is a massive difference, not a typo. I am saying that if that is the case, then maybe the hamming distance between correct and buggy code that both compile should be greater than 1, regardless if more tooling can help solve the problem or not.

I specifically take issue with this framing of it is not an issue for we have the tools to help with this, especially where the tools are not part of a standard distribution of a toolchain and require more than minimal effort. C++ has had many a warts for many decades, and the response has always been *you are just holding it wrong* and not running a well covering integration test suite with sanitizers on every commit, you just need to run one more tool in the CI, just a comprehensive benchmarking suite, have more eyes looking for a single char difference in reviews.

Re: The repercussions of missing an Ampersand in C++ and Rust

#35

With Rust executing a function for either case deploys the “optimal” version (reference or move) by default, moreover, the compiler (not the linter) will point out the any improper “use after moves”. struct Data { // Vec cannot implement "Copy" type data: Vec , } // Equivalent to "passing by const-ref" in C++ fn BusinessLogic(d :&Data) { d.DoThing(); } // Equivalent to "move" in C++ fn FactoryFunction(d: Data) -> Own…

> could physically copy d despite it being non-Copy. Is this correct?

I believe the answer is technically yes. IIRC a "move" in Rust is defined as a bitwise copy of whatever is being moved, modulo optimizations. The only difference is what you can do with the source after - for non-Copy types, the source is no longer considered accessible/usable. With Copy types, the source is still accessible/usable.

Re: The repercussions of missing an Ampersand in C++ and Rust

#36

With Rust executing a function for either case deploys the “optimal” version (reference or move) by default, moreover, the compiler (not the linter) will point out the any improper “use after moves”. struct Data { // Vec cannot implement "Copy" type data: Vec , } // Equivalent to "passing by const-ref" in C++ fn BusinessLogic(d :&Data) { d.DoThing(); } // Equivalent to "move" in C++ fn FactoryFunction(d: Data) -> Own…

Well since you're saying "physically" I guess we should talk about a concrete thing, so lets say we're compiling this for the archaic Intel Core i7 I'm writing this on.

On that machine Data is "physically" just the Vec, which is three 64-bit values, a pointer to i32 ("physically" on this machine a virtual address), an integer length and an integer capacity, and the machine has a whole bunch of GPRs so sure, one way the compiler might implement FactoryFuncton is to "physically" copy those three values into CPU registers. Maybe say RAX, RCX, RDX ?

Actually though there's an excellent chance that this gets inlined in your program, and so FactoryFunction never really exists as a distinct function, the compiler just stamps out the appropriate stuff in line every time we "call" this function, so then there was never a "parameter" because there was never a "function".

Re: The repercussions of missing an Ampersand in C++ and Rust

#38
post #4

Rust's behavior of moving without leaving a moved-out shell behind also simplifies the implementation of the type itself, because its dtor doesn't have to handle the special case of a moved-out shell, and the type doesn't even need to be able to represent a moved-out shell. For example, a moved-out-from tree in C++ could represent this by having its inner root pointer be nullptr, and then its dtor would have to check…

This was one of the most unsatisfying things about learning C++ move semantics. They only kinda move the thing, leaving this shell behind is a nightmare.

Since I use move semantics all the time, this is for me the most frustrating thing about C++ full stop. I really wish they'd fix this instead of adding all those compile-time features.

Re: The repercussions of missing an Ampersand in C++ and Rust

#39
post #33

With Rust executing a function for either case deploys the “optimal” version (reference or move) by default, moreover, the compiler (not the linter) will point out the any improper “use after moves”. struct Data { // Vec cannot implement "Copy" type data: Vec , } // Equivalent to "passing by const-ref" in C++ fn BusinessLogic(d :&Data) { d.DoThing(); } // Equivalent to "move" in C++ fn FactoryFunction(d: Data) -> Own…

Can't run Godbolt on my phone for some reason, but in this case I expect compiler to ignore wrapper types and just pass Vec around. If you have Vec // newtype struct struct Data{ data: Vec } // newtype enum in rust // Possibly but not 100% sure // enum OneVar { Data(Vec ) } From my experiments with newtype pattern, operations implemented on data and newtype struct yielded same assembly. To be fair in my case it wasn'…

The compiler isn't ignoring your new types, as you'll see if you try to pass a OneVar when the function takes a Vec but yes, Rust really likes new types whose representation is identical yet their type is different.

My favourite as a Unix person is Option. In a way Option is the same as the classic C int file descriptor. It has the exact same representation, 32 bits of aligned integer. But Rust's type system means we know None isn't a file descriptor, whereas it's too easy for the C programmer to forget that -1 isn't a valid file descriptor. Likewise the Rust programmer can't mistakenly do arithmetic on file descriptors, if we intend to count up some file descriptors but instead sum them in C that compiles and isn't what you wanted, in Rust it won't compile.

Re: The repercussions of missing an Ampersand in C++ and Rust

#40
post #22

I would never have this typo as I usually delete the copy constructor in heavy structures.

this is the defensive and correct C++ approach, anyways.

Isn't that just same old "skill issue", "No True C(++) programmer" refrain?

If people could keep entirety of J.2 appendix in their mind at all time we would not have these issues. And if they had entirety of J appendix in mind all C code would be portable.

Or if people just always ran -Wall -Wpedantic -Wall_for_real_this_time -fsanitize=thread,memory,address,leaks,prayers,hopes,dreams,eldritch_beings,elder_gods -fno-omit-frame-pointer

I mean if this was all it took then C and C++ programs would be as safe as Rust. Which is not what we see in practice. And it's not like C programmers are an average web dev. It's a relatively niche and well versed community.

Post reply on HN