The problem in the article is that the code has a firm RAII promise that the higher level object will be destroyed correctly (i.e. never before any called functions use it). But the author wants to add safety by ensuring that the called functions aren't able to store or copy the pointer or otherwise muck with that allocation promise. And the chosen syntax with unique_ptr works well enough I guess (though it will break if you need the same object used across two disjoint subtrees of the calll tree).
Personally? If this is really what you want then Rust is probably a better proposition as it doesn't require extra syntax for this case. Is it really what you want? Dunno. A simple application of "const" to a few places can get you maybe 70% of the way to the goal here in designs like this.
Yes, while I prefer to use plain values or unique_ptrs when I can, I will use C++ references (or plain pointers) in this sort of situation. Basically in C++ I think an of old-style pointers/refs as Rust-like "borrowed" references.
This is an example of how the Rust safety model is really about compiler enforcement of good practices from C and C++ (as opposed to, say, GC which is a distinct practice). I appreciate what Rust is doing, but it is also true that good coding conventions in C++ will get you 90% of the way there.
For those that need it, SaferCPlusPlus conventions will get you even further.
> Basically in C++ I think an of old-style pointers/refs as Rust-like "borrowed" references.
They key safety feature of Rust references is that they have "scope lifetime" and their target is guaranteed to be alive for the duration of that scope lifetime. You can use SaferCPlusPlus' "scope pointers" to achieve this in C++.
Scope pointers can be explicitly converted to raw pointers, so they can be used with existing "legacy" functions. But you get even more benefit when you change your function interfaces to support scope pointers directly. For example, it would prevent the function from (inappropriately) putting a copy of the scope pointer into "long term storage" (like Rust does).
 shameless plug: https://github.com/duneroadrunner/SaferCPlusPlus#scope-point...
 Whether a resource is allocated on the heap or elsewhere, or, whether a heap-allocated resource has owned or reference-counted semantics, are implementation details that should be abstracted away from your top-level APIs, so you have the flexibility to change them later.
I've found using unqiue_ptr + proper move semantics tend to lead to cleaner, single-owner architectures that are much more sane to maintain/scale.
I'm not against smart pointers where they make sense, it's just whenever I see them I start bracing myself for the horrors that seem to inevitably follow.
It's also why I really like that Rust decided to go RAII/single-ownership and add a ton of friction(via RefCell) to make sure that you really want shared ownership. It forces you to get your house in order up-front which I've found almost always leads to a better design overall.
In C++'s defense it's not like the language or the compilers encourage you to use shared_ptr, but it's also true (mainly for legacy C-interop cruft) it doesn't do enough to nudge you to write code in a proper RAII style, nor to enforce correctness WRT ownership.
I like that Rust forces you to explicitly opt out of RAII and/or single-owner semantics. (Those are the "defaults" in C++ too, but too easy to implicitly or accidentally opt out of). On the other hand you can write Java in any language, and from the little Rust I've read, newcomers from managed langs will often daisy-chain enough smart pointers in a row to put any 13-year-old who just downloaded Unreal Engine to shame.
The last time I took a serious look at Rust I thought it took things too far in the other direction; you weren't able to use two (nonoverlapping!) data members of the same struct in a function, and the suggested workaround was to wrap it in a smart pointer. Has that issue been fixed now?
I think that's largely been addressed. Are you talking about doing something like this?
Something else to keep in mind that while it's nice to have the borrow-checker validate everything it's okay to step around it if you've got a case that's gnarly and you know is valid. I've had some C callback stuff where I run into that occasionally, I can't encode the lifetime semantics to the calling function so I have to side-step it.
In that case things like RefCell(which are separate from (A)Rc/Box) are a great "escape hatch".
Obviously you should try and keep it to a minimum but don't let it be a complete roadblock.
This is especially frustrating when it's self that is being captured. There's an accepted RFC for fixing this but I'm not sure when it's going to be implemented.
Agreed that it's not the best default behavior though. Generally I tend to try to keep unrelated things in separate structs for this reason(with the nice side benefit of keeping concerns separate).
While that worry sounds insane, it is exactly the kind of thing C compilers have retroactively defined as Undefined Behaviour. So kudos to Rust for having the compiler tell you up front where it reserves the right to do stupid stuff in future.
I dont't think it's as bad as it sounds, and I have an explanation: C++ has evolved to the point when the ease of coding in it is comparable with that of Java; so, more people use C++ as if it was Java, and shared_ptr is the closest thing to garbage collection that C++ standard library provides.
You haven't lived until you've root-caused a ref cycle 3 libraries deep in a 200kloc+ codebase.
This problem also isn't exclusive to C++, I've had nasty libraries in Java add things to the root set silently to the same effect.
Although it seems to me that most people seem to be proponents of actually sitting down and working out object ownership of your classes rather than just being lazy and using smart pointers everywhere.
I'm still experimenting with this approach, definitely slows me down right now but I imagine that's because I haven't done it enough. Not going to lie, coming from Java/C#, it's really convenient just being able to 'new' an object and not worry much about its lifetime.
Doesn't that only work when the parameter is movable? For older classes that don't yet have move semantics, unique_ptr is an easy wrapper to give them efficient move semantics, is it not?
I agree that these methods should just take rvalue references, though. Doesn't really matter for unique_ptr, but for other things like shared_ptr it adds up quick.
Sometimes, but not in this case, because
- Smart pointers are nullable, which should be opt-in and not opt-out behavior.
- Smart pointers have pointer-like interfaces, which do not read like idiomatic modern C++. A wrapper class `GadgetHandle` can define conversion operators for `Gadget&` and `const Gadget&` so their interface reflects their semantic meaning (a `Gadget`) and hides irrelevant or unsafe implementation details (the operators and methods belonging to the smart pointer class).
- As a corollary to the above, you can create several classes `UniqueGadgetHandle`, `SharedGadgetHandle`, `CustomGadgetHandle` and so forth, which enable you to very easily and transparently intermix or alter allocation strategies (even on the stack or statically) for your `Gadget`s, usually without needing to alter any public APIs or more than one section of your codebase.
- When your heap-allocated data [structures] don't semantically correspond to the containers provided by the STL or your other library dependencies, I find that chances are you have or may add some custom constructor/destructor logic beyond just new and delete.
Isn't this what a `unique_ptr` is (a RAII class that manages Heap allocation)?
> sink functions" should take an rvalue reference (which semantically means "take ownership of this parameter").
Why not take by value?
See my reply to kllrnohj above: https://news.ycombinator.com/item?id=15872509
>Why not [write sink functions that] take by value?
That could work in the case where the parameter is a class which is movable but not copyable, but I'd still prefer an rvalue reference in that case because it makes your meaning obvious without having to refer to the class definition or documentation.
You mean using move semantics?
Instead of improving a detail, you can just improve everything and move to Rust... :>