The advantage is that the allocations are grouped: they're allocated in the same memory region (good memory locality) and freed in bulk. The tradeoff is needing to explicitly create these scopes and not being able to have custom deallocation logic like you can in a destructor.
(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)
You can certainly do it with RAII. However, what if a language lacks RAII because it prioritizes explicit code execution? Or simply want to retain simple C semantics?
Because that is the context. It is the constraint that C3, C, Odin, Zig etc maintains, where RAII is out of the question.
Ok then I understand what you mean (I couldn't respond directly to your answer, maybe there is a limit to nesting in HN?).
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.
The idea is, you could have a language like Rust, but with linear rather than affine types. Such a language would have RAII-like idioms, but no implicit destructors; instead, it'd be a compile-time error to have a non-Copy local variable whose value is not always moved out of it before its scope ends (i.e., to write code that in Rust could include an implicit destructor call). So you would have explicit deallocation functions like in C, but unlike in C you could not have resource leaks from forgetting to call them, because the compiler would not let you.
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.
It seems like exactly the same verbosity as what you'd do with a custom allocator.
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.
The benefit is that it: (a) works in a language without RAII, and C-like languages usually does not have that (b) there are no individual heap allocations and frees (c) allocations are grouped together.
*There are no “objects” in the OOP sense, and as such you can say it has “no constructors and no destructors”, but Rust doesn't have “objects” either and neither does it has constructors, yet there's zero doubt Rust is using RAII for memory management (Rust has destructors though, but they aren't generally being used for “memory management” but for broader “resource management”, like network sockets or file descriptors).
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.
There are a few misconceptions in your comment that are very common, so I hope you'll allow me to clarify them and in the process hopefully explain why this sort of "parallel stack on the heap" approach is not the same as RAII at all.
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.
If you're interested, the Austral language (not my creation, but I'm a fan of the author's work) has ownership semantics where freeing is always explicit and in the hands of the programmer, but it's still safe like Rust.
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.
@pool appears to be exactly what C++ does automatically when objects fall out of scope.