@pool appears to be exactly what C++ does automatically when objects fall out of scope.
@pool appears to be exactly what C++ does automatically when objects fall out of scope.
(This doesn't seem to have anything to do with borrow checking though, which is a memory safety feature not a memory management feature. Rust manages memory with affine types which is a completely separate thing, you could write an entire program without a single reference if you really wanted to)
Because that is the context. It is the constraint that C3, C, Odin, Zig etc maintains, where RAII is out of the question.
To the extent that you subscribe to a principle like "invisible function calls are never okay", this solves that without undermining Rust's safety story more broadly. I have no idea whether proponents of "better C" type languages have this as their core rationale; I personally don't see the appeal of that flavor of language design.
It is about types that can't be copied and can't go out of scope, and the only way to destroy them is to call one of their destructors. This is compile time checkable.
In theory they can solve a lot of problems easily, mainly resource management. Also it generalizes C++'s RAII, and similar to Rust's ownership.
In practice they haven't got support in any mainstream programming language yet.
Let me respond in some more detail then to at least answer why C3 doesn't have RAII: it tries to the follow that data is inert. That is – data doesn't have behaviour in itself, but is acted on by functions. (Even though C3 has methods, they are more a namespacing detail allowed to create methods that derive data from the value, or mutate it. They are not intended as organizational units)
To simplify what the goal is: data should be possible to create or destroy in bulk, without executing code for each individual element. If you create 10000 objects in a single allocation it should be as cheap to free (or create) as a single object.
We can imagine things built into the type system, but then we will need these unsafe constructs where a type is converted from its "unsafe" creation to its "managed" type.
I did look at various cheap ways of doing this through the type system, but it stopped resembling C and seemed to put the focus on resource management rather than the problem at hand.
So that is why it's closer to C than Rust.
I think the only real grace is you don't have to pass around the allocator. But then you run into the issue where now anyone allocating needs to know about the lifetimes of the pool of the caller. If A -> B (pool) -> C and the returned allocation of C ends up in A, now you potentially have a pointer to freed memory.
Sending around the explicit allocator would allow C to choose when it should allocate globally and when it should allocate on the pool sent in.
Rust doesn't even have a good allocator interface yet, so libraries like bumpalo have a parallel implementation of some stdlib types.
I'm confused: how is it not exactly RAII?
If you really don't want to call them objects, there are at least “items” that are “created” and then “deallocated” based on their lexical scope, which is exactly what Rust does too for its RAII.
Lets ignore the horrifically named C++ feature for a moment and speak more generally of "ownership".
Ownership (in the domain of programming languages) is kind of relation between resources. If resource A is the sole owner of resource B, then relinquishing resource A also relinquishes resource B, but not vice versa. It gives a form of agency to resources: "Hey A, it's your job to take care of B alright?"
Note that there's nothing about "lexical scope" in the description above. In fact, tying ownership to lexical scope isn't necessary at all. For example, I could do `new std::vector<std::string>()` in C++, fill it up with a bunch of strings, and then call `delete` on it at my leisure. No lexical scope involved.
Tying freeing to lexical scope is a convenience, a way to automate the freeing of resources, but it's not required by "ownership semantics" at all. In fact, the idea that freeing is tied to "lexical scope" is incorrect even when considering the automation provided by Rust and C++.
Both Rust and C++ have the concept of "moving". If an object is moved, then its lifetime is now tied to a different scope (if it was returned for example) or to another resource.
If you move a resource in C++, it's destructor at the end of scope will be a no-op. If you move a resource in Rust, it won't add a destructor call at the end of scope at all. If the move was conditional (only happens on a subset of branches) rust will actually add a little boolean variable tracking this, and then check that variable before calling the destructor at the end of the scope.
This last bit is actually only necessary because Rust wants to tie the freeing of the resource to end of scope. Freeing at end of scope isn't necessary at all. Rust could free the resource right at the point of last use, the language has excellent lifetime tracking it could use for this.
The reason Rust places the call to the destructor at the end of the scope is to make unsafe less unsafe. If there's a raw pointer to that data and it might get deleted at any point then, well, that's very dangerous. If you know the free will only happen at end of scope, then you can work with that.
So what do I want to get across with all of this? Ownership (RAII) is a way to tie resources together that is only incidentally related to lexical scopes. It gives resources agency by allowing them to manage other resources, via custom destructors.
What C3 has (for memory management only, it uses defer for other resources) is just a way to have a parallel stack on the heap. There's no "ownership" in the same way the stack isn't really an "owner" in the same sense as other resources. There are no destructors, data is inert.
All allocations that happen within that parallel stack are freed in bulk (not one by one, a single big free), because the only real "resource" is the parallel stack itself. You cannot "move" this parallel stack, and is not managed by another parallel stack, there's no ownership.
If you want the same functionality in Rust you need to use a crate like Bumpalo. In C++ you'd use std::pmr::monotonic_buffer_resource. RAII is strictly more powerful since you can model these parallel stacks in it, but it's also not equivalent since you can't have cycles without implementing this first.
Because both the destructor calls and the "borrowing" scopes have to be written explicitly in the language, it really helps in understanding how it all fits together.