Hacker News new | ask | show | jobs
by steveklabnik 1650 days ago
> In order to have a reasonably complete language that has RAII and value types, you must also have: - constructors - destructors - overloadable copy assignment operators - placement new - move semantics and rvalue references

Rust has RAII and value types, and does not have constructors, overloadable copy assignment operators, placement new, or rvalue references (though we do of course have a very similar notion to rvalue/lvalue in general, but that's not the same thing as "rvalue references" with relation to all of this). While it has move semantics, they're significantly different.

1 comments

I don't mean that these things need to manifest in exactly the same way as they do in C++, but analagous features are needed. You're right that rvalue references are not necessary, but some form of move semantics are. When I say constructors and destructors, I am really referring to having a concept of object lifetimes as part of the language. Zig does not have this, and is much simpler because of it.

Edit: to clarify, the thing that makes a constructor/destructor useful in this case is the property that it begins/ends an object lifetime according to the language. This lifetime reasoning certainly has benefits, like the ability to have const fields in C++ and the ability to do static checking of lifetimes in Rust. However it also comes with significant complexity, because move semantics are needed throughout the language, and begin/end lifetime tags are needed when implementing data structures that use preallocated backing arrays.

Hm, personally I consider "object lifetimes exist" to be completely different than "constructors", which are a hook into a specific point in some sort of object lifetime cycle. Rust doesn't have the hook, so it doesn't have the feature. Note that I didn't put destructors on my list; the Drop trait does exist in Rust and is the same general idea as destructors.

I guess that basically, to me at least, if you've stretched the definitions of these features far enough to include what Rust does, you don't really have a meaningful definition any more.

That's a fair criticism, I think C++ blurs a lot of lines that make these things difficult to talk about in isolation, and I'm still working on being more rigorous about picking them apart correctly. The very specific complexity at the core of it all is the fact that you need something like placement new to begin a lifetime in memory that is already allocated. Copying the object representation of an initialization template is insufficient in general, you have to use a specific tag to say "the life of a new object starts here", which has no direct analogy to anything the hardware does. It's not necessarily important that the programmer can hook into this like a C++ constructor, but the tag needs to be there. This is something that can be very difficult for programmers to get right in languages that aren't Rust, because the compiler does no verification of it. If you do it wrong, everything works in debug, but then things may break in release because the optimizer performed incorrect alias analysis or detected unconditional UB. But they might work correctly, you may not notice for years until a more powerful optimizer comes along. Since we don't plan to validate lifetimes (Rust is a great language that already does that if it's important to your goals), we would like to avoid this sort of strict object model as much as possible.

When you add RAII on top of lifetimes as a language feature, it creates the need for language support for moves and specialized copies. Or a need to say that types cannot be copied. But you need some sort of tag that says "memcpy doesn't cut it anymore", which is what I mean by overridable copy behavior.

> That's a fair criticism, I think...

Totally, and I wouldn't be so pedantic here myself if I didn't think it was on-topic: Zig, Rust, and C++ all choose different amounts of complexity on these axes. I think that Rust's RAII is closer to Zig's lack of it than C++'s implementation of it in terms of overall complexity, but that the feature exists at all in Rust is significant. All of that should come as no surprise. :)

> The very specific complexity at the core of it all is the fact that you need something like placement new to begin a lifetime in memory that is already allocated.

By this definition, Rust doesn't have RAII. Placement new does not currently exist in the language. This is possible because we do not have constructors, and therefore don't need (on the language level, I'll come back to this momentarily) the need to do this. It does mean that, as you've noted, copying it is possible, and this is what happens in Rust. Optimizers can elide this copy but aren't guaranteed to. But the need to eliminate this single copy hasn't been big enough to actually get placement new over the finish line in Rust, even though at one point it felt critical to even shipping 1.0.

Ah interesting, yeah I suppose if we define "RAII" as just "automatically invoked configurable cleanup", there's no actual need to mark the beginning of the lifetime. All you need is to mark the end of the lifetime. This needs to be possible both from the language (for stack variables) and from user code (for ArrayList implementations). So you need something like a placement delete or explicit destructor invocation, but not necessarily a placement new or explicit creation.

I think I mix these up because placement new is necessary to have a concept of const fields that is meaningful to the optimizer. This would have been an alternate solution to the original vtable problem, but requires lifetimes over which the field is const. I had RAII categorized as another feature that required lifetimes, but I suppose it requires a less strict definition of lifetimes than is needed for const fields.

However the Drop trait still has some safety issues, right?

It appears to be more like Ada's Unchecked_Deallocation as it comes with some "use with care" footnotes.

I remember reading something about it.

Not exactly, or at least, not in the same way as Unchecked_Deallocation. There are no memory safety issues with Drop. There are a few times when you need to be a little careful so that you don't get bad behavior: recursive Drop can stack overflow (which is caught and aborts), Drop while Dropping aborts which may not be what you want, if you've written unsafe code, you need to be careful with Drop because what is valid and what isn't may be a bit more tricky since it's effectively a callback.
I see, then I misunderstood whatever was related to Drop and not being fully stable.

Yeah stack overflow can also be an issue on other RAII approaches.

Hm yeah, on the "not fully stable" angle, there's also some talk about "async drop," which doesn't exist just yet, and would be useful in async contexts. Maybe that's what it was.