This has a section on Object Safety, I checked and the article was written this week, but "Object Safety" is a confusing name for this idea, and so for a little while now Rust calls this idea "dyn compatibility" because the most important thing you're getting if a trait is "dyn compatible" is that you can use "dyn Trait" - https://doc.rust-lang.org/1.98.1/reference/items/traits.html...
That link more comprehensively explains the rules too.
[Edit: Realized the end of the article explains this, the author began writing it months ago, likely before they read about the improved "dyn compatibility" naming]
loeg 6 hours ago [-]
The aside about C++ seems a little confused too?
> C++ doesn’t have this problem at all, since virtual dispatch always goes through pointers and return types are always pointers too.
Uh. C++ can return objects by value. Maybe it isn't idiomatic. And I don't know if there's a convenient spelling like `Self`. But it runs into the same problem, of course -- you would need to know the concrete value type returned to have storage for it.
And Rust has the ~same solution as imagined for C++ here, I think? Have a `DynClone` trait that returns `Box<dyn Trait>` instead of `Self`.
neerajsi 6 hours ago [-]
I agree with what you're saying. It's called: https://en.wikipedia.org/wiki/Object_slicing. Clearly a footgun in c++. You have to return a pointer to something that is not locally allocated.
gmueckl 3 hours ago [-]
Polymorphism in C++ can only be correct on pointers and references (which are pointers internally).
Stack-allocated objects and polymorphism only combine if the object is initialized as its true type and a pointer or reference to it is handed out. Obviously, this can create object lifetime issues if the pointer or reference escapes the lifetime of the stack frame containing the object.
quotemstr 3 hours ago [-]
What does your explanation have to do with the fact that C++ can express by-value returns of complex objects?
evmar 3 hours ago [-]
In my own journey of discovery I found https://cheats.rs/ very helpful, and in particular its "memory layout" section has visualizations. (No affiliation with the site, just a happy reader!)
Waterluvian 3 hours ago [-]
I never did well in English and I usually don’t know it when I see it. But it was immediately evident that the writing style of this blog was sparking joy. Even the intro section was just so well structured that I had to read it again.
returningfory2 6 hours ago [-]
Very nice. As a follow up would be interesting to also reverse engineer the structure of the vtable itself. I guess it’s a list of pointers to the method implementations?
> In Rust, that question is answered by the borrow-checker at compile time:
(About zero sized objects being the same)
But why does that mean the programmer never has to check? (or if they want that information from the borrow checker how would they get it?) It's not motivated as the intro above for c++ was just "In C++, we might do this to check if two pointers refer to the same object".
So the borrow checker knows already, why does that stop the programmer from wanting to know or separate these cases?
steveklabnik 7 hours ago [-]
> if they want that information from the borrow checker how would they get it?
Rust just defaults to value equality over reference equality. This is true for everything, not just ZSTs.
(I find the post's framing of "it's stored in the borrow checker" to be a bit odd, but I can't put my finger on exactly what it is. The borrow checker doesn't determine these sorts of semantics, it checks for liveliness and aliasing, so "do these pointers alias" isn't inherently not the borrow checker's job, it just strikes me as an odd way to put it. Maybe it's because you don't "ask the borrow checker for that information" really.)
ketzu 7 hours ago [-]
As the article itself discusses, pointers to zero sized objects are not necessarily different (they write it is only the case in debug mode).
> I find the post's framing of "it's stored in the borrow checker" to be a bit odd
That's exactly what I wanted to say as well.
I feel like it would have been better to just skip the borrow checker mention and just go "in rust this can not be done reliably ..(section about pointers being the same)"
7 hours ago [-]
TazeTSchnitzel 7 hours ago [-]
I'm not sure if this quite answers your question, but the difference in philosophy here is that C++ objects with pointers always have identity, whereas a Rust object only has identity if it has a non-zero size. It's an application of the zero-overhead principle, "you only pay for what you use".
phire 44 minutes ago [-]
> whereas a Rust object only has identity if it has a non-zero size.
Rust also has the complication that function pointers are not guaranteed to have an unique identity; If multiple functions compile to the same code, the compiler is allowed de-duplicate them.
The documentation [0] also warns it's also possible for the compiler to create multiple versions of the same function. And while I've absolutely seen the compiler to create multiple optimised versions of functions in disassembled code (partial inlining based on the caller), I'm not sure it's possible to get pointers to more than one version.
"zero sized objects don't have identity" actually would answer my question and is a really interesting factoid, thanks!
tialaramex 7 hours ago [-]
This is why C++ doesn't have ZSTs, it wants all objects to have identities, the obvious way to distinguish them is by where they are in memory, but ZSTs don't have distinct addresses in memory.
ameliaquining 40 minutes ago [-]
I'm not a C++ expert. Why does C++ want all objects to have identities? Presumably it has some feature or something Rust doesn't have that requires this?
7 hours ago [-]
wrs 7 hours ago [-]
I've tried (admittedly for just a few minutes) to come up with a case where you'd need to compare two pointers to ZSTs in any real algorithm, and failed. Can you think of one?
loeg 8 hours ago [-]
It's not obvious to me that the borrow checker actually knows, so much as it can prove there are no illegal conflicts between mut and non-mut references to the same object.
If you somehow need this property in your programs, I think you can just add a 1-byte member and use pointer equality. (I'm not a Rust expert.)
wahern 16 minutes ago [-]
I assumed it's a figure of speech, but, yeah, the borrow checker only enforces function-local invariants, with no interprocedural analysis or memorization. Parameter aliasing is just a logical deduction from assuming local invariants are maintained at all call sites. That's why the borrow checker imposes very minimal cost (esp when it was block based), and doesn't even require static compilation. Traits and other aspects of the type system are where the complexity and slow compile times come from.
I guess maybe the semantics of ZSTs complicate the aliasing story a tad?
7e 3 hours ago [-]
The title includes the word “visualizing” but there is not a single diagram in this article. Is English not the first language of the author?
Rendered at 04:14:39 GMT+0000 (Coordinated Universal Time) with Vercel.
That link more comprehensively explains the rules too.
[Edit: Realized the end of the article explains this, the author began writing it months ago, likely before they read about the improved "dyn compatibility" naming]
> C++ doesn’t have this problem at all, since virtual dispatch always goes through pointers and return types are always pointers too.
Uh. C++ can return objects by value. Maybe it isn't idiomatic. And I don't know if there's a convenient spelling like `Self`. But it runs into the same problem, of course -- you would need to know the concrete value type returned to have storage for it.
And Rust has the ~same solution as imagined for C++ here, I think? Have a `DynClone` trait that returns `Box<dyn Trait>` instead of `Self`.
Stack-allocated objects and polymorphism only combine if the object is initialized as its true type and a pointer or reference to it is handed out. Obviously, this can create object lifetime issues if the pointer or reference escapes the lifetime of the stack frame containing the object.
(About zero sized objects being the same)
But why does that mean the programmer never has to check? (or if they want that information from the borrow checker how would they get it?) It's not motivated as the intro above for c++ was just "In C++, we might do this to check if two pointers refer to the same object".
So the borrow checker knows already, why does that stop the programmer from wanting to know or separate these cases?
If you want to compare if two pointers point to the same place, you use https://doc.rust-lang.org/stable/std/ptr/fn.eq.html
Rust just defaults to value equality over reference equality. This is true for everything, not just ZSTs.
(I find the post's framing of "it's stored in the borrow checker" to be a bit odd, but I can't put my finger on exactly what it is. The borrow checker doesn't determine these sorts of semantics, it checks for liveliness and aliasing, so "do these pointers alias" isn't inherently not the borrow checker's job, it just strikes me as an odd way to put it. Maybe it's because you don't "ask the borrow checker for that information" really.)
> I find the post's framing of "it's stored in the borrow checker" to be a bit odd
That's exactly what I wanted to say as well.
I feel like it would have been better to just skip the borrow checker mention and just go "in rust this can not be done reliably ..(section about pointers being the same)"
Rust also has the complication that function pointers are not guaranteed to have an unique identity; If multiple functions compile to the same code, the compiler is allowed de-duplicate them.
The documentation [0] also warns it's also possible for the compiler to create multiple versions of the same function. And while I've absolutely seen the compiler to create multiple optimised versions of functions in disassembled code (partial inlining based on the caller), I'm not sure it's possible to get pointers to more than one version.
[0] https://doc.rust-lang.org/std/ptr/fn.fn_addr_eq.html
If you somehow need this property in your programs, I think you can just add a 1-byte member and use pointer equality. (I'm not a Rust expert.)
I guess maybe the semantics of ZSTs complicate the aliasing story a tad?