This isn't a C++ vs. Rust thing. If you care about performance, you measure it. If you don't measure performance, you don't care about it.
The repercussions of missing an Ampersand in C++ and Rust
41–50 of 128 posts
Re: The repercussions of missing an Ampersand in C++ and Rust
#42This isn't a C++ vs. Rust thing. If you care about performance, you measure it. If you don't measure performance, you don't care about it.
You have to do it correct or you might be just measuring: when your system is pulling updates, how big is your username, the performance of the least critical thing in your app.
And at worst you can speed up your least performing function only to yield a major slowdown to overall performance.
Re: The repercussions of missing an Ampersand in C++ and Rust
#43Earlier quoted context omitted.
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.
When I looked into the history of the C++ move (which after all didn't even exist in C++ 98 when the language was first standardized) I discovered that in fact they knew nobody wants this semantic. The proposal paper doesn't even try to hide that what programmers want is the destructive move (the thing Rust has) but it argues that was too hard to do with the existing C++ design so... The more unfortunate, perhaps dis…
> The more unfortunate, perhaps disingenuous part is that the proposal paper tries to pretend you can make the destructive move later if you need it once you've got their C++ move.
For reference, I think N1377 is the original move proposal [0]. Quoting from that:
> Alternative move designs
> Destructive move semantics
> There is significant desire among C++ programmers for what we call destructive move semantics. This is similar to that outlined above, but the source object is left destructed instead of in a valid constructed state. The biggest advantage of a destructive move constructor is that one can program such an operation for a class that does not have a valid resourceless state. For example, the simple string class that always holds at least a one character buffer could have a destructive move constructor. One simply transfers the pointer to the data buffer to the new object and declares the source destructed. This has an initial appeal both in simplicity and efficiency. The simplicity appeal is short lived however.
> When dealing with class hierarchies, destructive move semantics becomes problematic. If you move the base first, then the source has a constructed derived part and a destructed base part. If you move the derived part first then the target has a constructed derived part and a not-yet-constructed base part. Neither option seems viable. Several solutions to this dilemma have been explored.
> In the end, we simply gave up on this as too much pain for not enough gain. However the current proposal does not prohibit destructive move semantics in the future. It could be done in addition to the non-destructive move semantics outlined in this proposal should someone wish to carry that torch.
[0]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n13...
Re: The repercussions of missing an Ampersand in C++ and Rust
#44Earlier quoted context omitted.
In practice, move operations typically just leave an empty object behind. The destructor already has to deal with that. And of course you can't call certain methods on an empty object. So in practice you don't need special logic except for the move operations themselves.
> The destructor already has to deal with that. That's partly true, partly circular. Because moves work this way, it's harder to make a class that doesn't have empty states, so I don't design my class to avoid empty states, so the destructor has to handle them.
Re: The repercussions of missing an Ampersand in C++ and Rust
#45> I was specifically inspired by a performance bug due to a typo. This mistake is the “value param” vs “reference param” where your function copies a value instead of passing it by reference because an ampersand (&) was missing ... This simple typo is easy to miss the difference between `const Data& d` and `const Data d` isn't accurately characterized as "a typo" -- it's a semantically significant difference in inten…
I think the problem with `T &d` and `T d` is that these 2 declarations yield a "name" `d` that you can operate on very similarly. It's not necessarily about reference declaration `T& d` is 1 char diff away compared to value declaration `T d`.
While there is a significant semantic difference between declaring things as a value and as a reference (&), non-static member function invocation syntax is the same on both `&d` and `d`. You can't tell the difference without reading the original declaration, and the compiler will happily accept it.
Contrast this to `T *d` or `T d`. Raw pointers require different operations on `d` (deref, -> operator, etc). You're forced to update the code if you change the declaration because the compiler will loudly complain about it.
It shares the same problem with a type system with nullable-by-default reference type vs an explicit container of [0..1] element Option. Migrating existing code to Option-type will cause the compiler to throw a ton of explicit errors, and it will become a breaking change if it was a public API declaration. On the other hand, you're never able to feel safe in nullable-by-default; a public API might claim it never return `null` in the documentation, but you will never know if it's true or not only from the type signature.
Whether it's good or bad, I guess it depends on the language designer's decision. It is certainly more of a hassle to break & fix everything when updating the declaration, but it also can be a silent footgun as well.
Re: The repercussions of missing an Ampersand in C++ and Rust
#46Rust'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.
Re: The repercussions of missing an Ampersand in C++ and Rust
#47Earlier quoted context omitted.
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 w…
True, I didn't meant to imply you can just ignore types; I meant to say that the equivalent operations on a naked vs wrapped value return equivalent assembly.
It's one of those zero cost abstraction. You can writ your newtype wrapper and it will be just as if you wrote implementations by hand.
> My favourite as a Unix person is Option.
Yeah, but that's a bit different. Compiler won't treat any Option that way out of the box. You need a NonZero type or nightly feature to get that[1].
That relies on compiler "knowing" there are some values that will never be used.
[1] https://www.0xatticus.com/posts/understanding_rust_niche/
Re: The repercussions of missing an Ampersand in C++ and Rust
#48With 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 t…
The point I am trying to make is more general:
I believe that when you have a type in Rust that is not Copy it will never be implicitly copied in a way that you end up with two visible instances but it is not guaranteed that Rust never implicitly memcopies all its bytes.
I have not tried it but what I had in mind instead of the Vec was a big struct that is not Copy. Something like:
struct Big {
buf: [u8; M],
}
// Make it non-Copy.
impl Drop for Big {
fn drop(&mut self) {}
}
From my understanding, to know if memory is shoveled around it is not enough to know the function signature and whether the type is Copy or not. The specifics of the type matter.Re: The repercussions of missing an Ampersand in C++ and Rust
#49Earlier quoted context omitted.
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 w…
> The compiler isn't ignoring your new types True, I didn't meant to imply you can just ignore types; I meant to say that the equivalent operations on a naked vs wrapped value return equivalent assembly. It's one of those zero cost abstraction. You can writ your newtype wrapper and it will be just as if you wrote implementations by hand. > My favourite as a Unix person is Option . Yeah, but that's a bit different. Co…
So if you make an enumeration AlertLevel with values Ominous, Creepy, Terrifying, OMFuckingGoose then Option is a single byte, Rust will assign a bit pattern for AlertLevel::Ominous and AlertLevel::Creepy and so on, but the None just gets one of the bit patterns which wasn't used for a value of AlertLevel.
It is a bit trickier to have Color { Red, Green, Blue, Yellow } and Breed { Spaniel, Labrador, Poodle } and make a type DogOrHat where DogOrHat::Dog has a Breed but DogOrHat::Hat has a Color and yet the DogOrHat fits in a single byte. This is because Rust won't (by default) avoid clashes, so if it asssigned Color::Red bit pattern 0x01 and Breed::Spaniel bit pattern 0x01 as well, it won't be able to disambiguate without a separate dog-or-hat tag, however we can arrange that the bit patterns don't overlap and then it works. [This is not guaranteed by Rust unlike the Option niche which is guaranteed by the language]
Re: The repercussions of missing an Ampersand in C++ and Rust
#50Earlier quoted context omitted.
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 t…
True. When I wrote the comment I did not think about the Vec though. The point I am trying to make is more general: I believe that when you have a type in Rust that is not Copy it will never be implicitly copied in a way that you end up with two visible instances but it is not guaranteed that Rust never implicitly memcopies all its bytes. I have not tried it but what I had in mind instead of the Vec was a big struct…
I will say that programmers very often have bad instincts for when that's a bad idea. If you have a mix of abilities and can ask, try it, who in your team thinks that'll perform worse for moving M = 64 or M = 32? Don't give them hours to think about it. I would not even be surprised to find real world experienced programmers whose instinct tells them even M = 4 is a bad idea despite the fact that if we analyse it we're copying a 4 byte value rather than copying the (potentially much bigger) pointer and taking an indirection
Edited: To fix order of last comparison