5 ms·
I'd argue it barely works in C++ as well. I've seen so many poorly implemented classes that violate the very complicated 3/5/0 rule. It's much easier to do RAII
by slaymaker1907 2y ago
I'd argue it barely works in C++ as well. I've seen so many poorly implemented classes that violate the very complicated 3/5/0 rule. It's much easier to do RAII correctly in Rust since people aren't constantly working with raw pointers and since objects which are moved somewhere else are not dropped like they are in C++.
One variant that I think might work even better than RAII or defer in a lot of languages is having a thread local "context" which you attach all cleanup actions to. It even works in C, you just define cleanup as a list of
typedef void(cleanup_function*)(void* context);
which is saved as into a thread local. Unlike RAII, you don't need to create a custom type for every cleanup action and unlike the call-with pattern from functional programming, lifetimes of these lists can be non-hierarchical.
However, I'm still glad to see defer being considered for C. It's a lot better than using goto for cleanup.
- pavlov 2y ago> 'a thread local "context" which you attach all cleanup actions to' Like the autorelease pool found in Objective-C of yore? I always liked that solution and sometimes implemented in plain C too.
- adrianN 2y agoYeah, the example of "what if you make a copy" breaks in C++ in exactly the same way if you're not careful.
- d0mine 2y agoArena can be used to allocate many times but deallocate exactly once. In Zig: test "arena allocator" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); _ = try allocator.alloc(u8, 1); _ = try allocator.alloc(u8, 10); _ = try allocator.alloc(u8, 100); }
- jay-barronville 2y agoI’m a fan of Zig, but I just want to point out that creating dedicated allocators for managing specific regions/chunks of memory or memory within specific application scopes (i.e., arenas) is just another memory allocation strategy rather than the ultimate solution to memory management issues. It comes with its own trade-offs and depends entirely on your use case. Also, it’s great that Zig has this battery included in its standard library, but arenas aren’t unique to Zig nor are they difficult to implement in any language that allows manual memory management. I’m just pointing this out because I keep seeing folks highlight this as a key feature of Zig over C.
- anymouse123456 2y agoThis. I've been loving Zig for some years now, but still write a lot of embedded C at work. I've started to use simple memory arenas in C and it just feels so damn _nice_. There's basically a shared lifetime for most of my transient allocations, which are nicely bounded in time by a "frame" of execution. Malloc/Free felt like a crazy amount of work, whereas an arena_reset(&ctx) just moves a pointer back to the first entry. Another person pointed out that arenas are not destructors, and this is a great point to make. If you're dealing with external resources, moving an arena index back to the beginning does not help - at all.
- OskarS 2y agoYou can do it in C for sure, but "culturally" in C, there's a stateless global allocator called "malloc", which is not the case in Zig. For instance, if you have a library libsomething in C, it will at most (probably) have something like this: #ifndef LIB_MALLOC #define LIB_MALLOC malloc #end if it allows you to customize allocation strategy at all, which is not a given. But this only allows you at compile time to provide your own stateless global allocator. This is very different in Zig, which has a very strong culture of "if something needs to allocate memory, you pass it a stateful, dynamically dispatched allocator as an argument". You COULD do that in C, but virtually nobody does.
- samatman 2y agoIt's 100% a key feature of Zig. Culturally, if it allocates, then it takes an allocator as an argument. C simply doesn't work that way. You could write C that way, but people don't. I've written reasonable amounts of both, and it's just different. For instance, in Zig, you can create a HashMap using a FixedBufferAllocator, which is a region of memory (which can be stack allocated) dressed up as an allocator. You can also pass it an arena and free all at once, or any other allocator in the standard library, or implemented by you, or anyone else. Show me a C library with a HashMap which can do all three of these things. Everything which allocates takes an allocator, third-party libraries respect this convention or will quickly get an issue or PR either requesting or implementing this convention. Ultimate solution? No, but also, sort of. The ability to idiomatically build a fine-grained memory policy is a large portion of what makes Zig so pleasant to use.
- sixthDot 2y agoallocation is not construction, and deallocation is not destruction. The two steps are oftently executed sequentially but if you think that they are the same you'll end up with leaks, e.g at the level of the operating system (e.g GDI handles). What I mean is that arena allocators are not as simple as you pretend. That depends on what they allocate. The more commonly reason why arena allocators are praised is the cache locality.
- Gibbon1 2y agoMy small brained comment is people use heap allocation when they should be using an arena allocation. And heap allocation shouldn't return a pointer it should return a handle.
- vlovich123 2y ago> that violate the very complicated 3/5/0 rule Is it actually complicated? There’s only the rule of 0 - either your class isn’t managing resources directly & has none of the 5 default methods defined explicitly (destructor, copy constructor/assignment, move constructor/assingment), or it manages 1 and exactly 1 resource and defines all 5. Following that simple rule gives you exception safety & perfect RAII behavior. Of all the things in C++, it seemed like the most straightforward rule to follow mechanically. BTW, the rule of 3 is from pre-C++11 - the addition of move construct/move assignment makes it the rule of 5 which basically says if you define any of those default ones you must define all of them. But the rule of 0 is far stronger in that it gives you prescriptive mechanical rules to follow for resource management. It’s much easier to do RAII correctly in Rust because of the ecosystem of the language + certain language features that make it more ergonomic (e.g. Borrow/AsRef/Deref) + some ownership guarantees around moves unless you make the type trivially copyable which won’t be the case when you own a resource.
- chipdart 2y ago> Is it actually complicated? It is. There is no point in arguing otherwise. To understand the problem, you need to understand why it is also a solution to much bigger problems. C++ started as C with classes, and by design aimed at being perfectly compatible with C. But you want to improve developer experience, and bring to the table major architectural traits such as RAII. This in turn meant you add support for custom constructors, and customize how your instances are copied and destroyed. But you also want to be able to have everything just work out of the box without forcing developers to write boilerplate code. So you come up with the concept of special member functions which are automatically added by the compiler if they are trivial. However, forcing that upon every single situation can cause problems, so you have to come up with a strategy that suits all use cases and prevents serious bugs. Consequently, you add a bunch of rules which boil down to a) if the class/struct is trivial them compilers simply add trivial definitions of all special member functions s that you don't have to, but once you define any of those special member functions yourself them the compiler steps back and let's you do all the work. Then C++ introduced move semantics. This refreshes the same problem as before. You need to retain compatibility with C, and you need to avoid boilerplate code, and on top of that you need to support all cases that originated the need for C++'s special member functions. But now you need to support move constructors and move assignment operators. Again, it's fine if the compiler adds those automatically if it's a trivial class/struct, but if the class has custom constructors and destructors then surely you also need to handle moves in a special way, so the compiler steps back and lets you do all the work. On top of that, you add the fact that if you need custom code to copy your objects around, surely you need custom code to move them too, and thus the compiler steps back to let you do all the work. On top of this, there are also some specific combinations of custom constructors/destructors/copy constructors/copy assignment operators which let the compiler define move constructors/move assignment operators. It all makes absolutely sense if you are mindful of the design requirements. But if you just start to onboard onto C++ and barely know what a copy constructors is, all these aspects are arcane and sadistic. If you declare nothing then your class instances are copied and moved automatically, but once you add a constructor everything suddenly blows up and your code doesn't even compile anymore. You spotted a bug where an instance of a child class isn't being destroyed properly, and once you add a virtual destructor you suddenly have an unrelated function call throw compiler errors. You add a snazzy copy constructor that's very performant and your performance tests suddenly start to blow up because of the performance hit if suddenly having to copy all instances instead of the compiler simply moving them. How do you sort out this nonsense? The rule of 5 is a nice rule of thumb to allow developers to have a simple mental model over what they need to do to avoid a long list of issues, but you still have no control over what you're doing. Things work, but work by sheer coincidence.
- bluGill 2y agoI've never seen someone get the rule of 5 wrong, but the rule of 3 was a reaction to 10 years of hindsight to realise the default is wrong. Congradulations to rust for looking to see what was done wrong by their predisessors. you can't really fault someone for making a mistake when nobody at the time knew it was a mistake.
- chipdart 2y ago> I'd argue it barely works in C++ as well. I've seen so many poorly implemented classes that violate the very complicated 3/5/0 rule. I'm afraid you're complaining about entirely unrelated things. It's one thing to claim that C++ structs have this or that trait. It's a entirely different thing to try to pin bugs and developer mistakes on how a language is designed.
- rramadass 2y ago> the very complicated 3/5/0 rule It is not very complicated at all; just a discipline to follow (or not if you know what you are doing) once learnt - https://en.cppreference.com/w/cpp/language/rule_of_three https://en.cppreference.com/w/cpp/language/rule_of_three Incidentally i use it as 4/6/0 by including the default ctor in the set.
- deleted 2y ago[deleted]
- uecker 2y agoI was initially interested in defer in C (I am co-author of some earlier proposal), but after actually studying its impact on code example I was entirely unimpressed about the actual improvement compared to goto style cleanup. A lot of people seem to like it though, and JeanHeyd's version seems quite good, but I personally not terribly convinced about this feature anymore.