Writing comments about pointer ownership
gpfault.net
gpfault.net
Some people might ask why I chose to make the parameter of BlackBox::store an rvalue reference rather than an lvalue reference and force the client code to use std::move. While it is true that an lvalue reference would also have worked, if I did that, it would be impossible to write code like crate.store(std::make_unique<Gadget>(beeper, flasher));. I also happen to think that forcing the client to use std::move at the call sites is good rather than bad, because it communicates the intended effect to the reader.
Passing unique_ptr by value is the correct option here, it will accept temporaries just fine.And in my opionion, if you want safety quarantees like that, use Rust. I was a fan of the new C++ standards a while ago, but after having used them for some time, I found C is a lot nicer language. There's just way too much mental overhead when trying to write "idiomatic C++" and the compiler errors you sometimes get are just a waste of developer time.
Some people might ask why I chose to make
the parameter of BlackBox::store an rvalue
reference rather than an lvalue reference
and force the client code to use std::move.
While it is true that an lvalue reference
would also have worked, if I did that, it
would be impossible to write code like
crate.store(std::make_unique<Gadget>(beeper, flasher));.
I also happen to think that forcing the client to use
std::move at the call sites is good rather than bad,
because it communicates the intended effect to the reader.Exactly. Instead of understanding the very simple concept of which object owns, I now have to understand that concept, how std::unique_ptr works, and rvalue references? How does it work with conditional ownage?
I also mostly use C instead of C++, but I admit that automatic constructor/destructor calls and virtual functions (as long as the class hierarchy doesn't become too deep) are quite useful. Lambda expressions too. I think that as long as you can use C++ while still having an idea what the language is doing implicitly what you would've had to do explicitly in C, you can get the advantages without the disadvantages.
While one concept is simpler, the other is controlled by compiler, while object ownership is only controlled by the developer himself.
There's some irreducible complexity with adding ownership semantics and the way standards committee ended up adding it to C++ is probably the most simplified way to accomplish it if you obey the constraint that the new compiler-assisted pointer ownership must coexist with legacy raw pointers and references.
To me, the complexity lies in the 30 years of un-annotated raw pointers in existing C++ code that must not be broken or rewritten if new ownership-style pointers are bolted on to the language.
I also think that another source of complexity/confusion is the label "move" in "std::move()". The word "move" is a verb and it seems to hint to beginners that some bytes changed position but unfortunately, that's not what's really happening. It might have been better to call it "std::rvalue()" -- the "rvalue" is a noun and you're telling the compiler that it is an "expiring value" that you promise to stop using. (Howard Hinnant[1] is the author of the std::move() C++ proposal and he gives good reasons why he called it "move" but I still think it's a confusing name.)
>, whereas in Rust it just works.
In C++, the defaults are "raw pointer" and unannotated references; the special cases are are unique_ptr, shared_ptr, &&, and std::move()
In Rust, the default is explicit borrow pointers, and the special-case pointers are raw pointers inside unsafe blocks.
Rust is "simpler" because it has the luxury of using a different baseline for its default case. C++ doesn't have that luxury.
It doesn't look that way from the GP's comment: "I really wish they had found a less confusing way to bolt this move semantics / rvalue references stuff onto C++ ..."
... is actually an example of not understanding (or not being aware of) historical pressure.
If you create a new programming language (e.g. Rust) with a blank slate, you have more flexibility to make semantics "simpler" by rethinking the design of the syntax, the sigils, and the implicit defaults.
Rust syntax accomplishes "move semantics" without double ampersands (&&) sigils and "std::move()".[1] The end result is that Rust syntax looks less cluttered to move ownership around.
The key to Rust's clean syntax is the absence of "historical pressure".
Rust starts from a blank slate without worrying about breaking legacy code. Rust wasn't invented in 1982.
C++ move semantics uses C++1982/C++98 as a starting point and can't use naked variables to let the compiler carry out "moves". Legacy code would be ambiguous and compilation would break. The "rvalues std:move()" was proposed in 2002 and finalized for C++11. They had 9 years for others to challenge Howard's proposal and devise a simpler more elegant way that doesn't break existing code. Nobody came up with an alternative. Hence, the uglier C++ syntax bolted on for "moves" is probably as simple as we're going to get.
[1]http://stackoverflow.com/questions/29490670/how-does-rust-pr...
No.
Eventually no one knows any longer how those comments map to the real code, because lots of those ever changing developers never update them.
Which leads to days delving into pointer related bugs.
If one is lucky to work in a shop that allows for modern C++, then it is great being able to use all the available features to avoid this type of situation.
I more or less do what unique_ptr does automatically, except with comments. I note "sources" and "sinks" of pointers... and work with them in that manner.
#include <memory>
struct Gadget;
struct BlackBox {
std::unique_ptr<Gadget> contents;
~BlackBox();
};
void func() {
BlackBox b;
}
This will fail to compile (for reasons I don't understand - can anyone explain?) But replace the unique_ptr with a raw pointer and it will be fine. /// Primary template of default_delete, used by unique_ptr
template<typename _Tp>
struct default_delete
{
...
void
operator()(_Tp* __ptr) const
{
static_assert(!is_void<_Tp>::value,
"can't delete pointer to incomplete type");
static_assert(sizeof(_Tp)>0,
"can't delete pointer to incomplete type");
delete __ptr;
}
};
Basically, invoking "operator delete" on a pointer to an incomplete type (aka opaque pointer) is usually undefined. Calling "operator new" on an incomplete type is not possible (because its size is 0). Since unique_ptr deals with allocation/deallocation, and doesn't just copy pointer values around, it's not fully comparable to a plain pointer.nice_byte proposed a good solution: if you can, abstract the new/delete logic to another file, in which the complete type of "Gadget" is known.
The missing puzzle piece is apparently exception handling: if the implicit constructor throws (which of course it cannot) it must delete this object (which of course will be null). Sadly this silly requirement persists even when exception handling is disabled at the compiler level.
Intention is to avoid undefined behaviour in situation where you delete an object with incomplete class type at the point of deletion, that has a non-trivial destructor or a deallocation function.
Edit: An deleter is used because of default generated copy constructor.
The reason it fails to compile is probably that the implicitly generate constructor of BlackBox (which is inline, I guess) may need to call the dtor of unique_ptr in case something in the constructor throws an exception.
But often you need weak_ptr, for caches - where shared_ptr won't free it, and unique_ptr won't allow it to be shared.
C++ is for people who work on large mature codebases, deal with practical issues, and actually build stuff. Talk to me about the virtues of Rust when you actually have real experience.
> C++ is for people who work on large mature codebases, deal with practical issues, and actually build stuff. Talk to me about the virtues of Rust when you actually have real experience.
According to the GitHub stats, I've added 293,494 lines of Rust code to Servo and 381,084 lines of code (mostly Rust) to the Rust compiler. I find that Rust's pointer ownership rules help improve the reliability and security of my code significantly.
Outside of you, the majority of "users" of Rust are script-kiddies who think it's "cool" (like Haskell cool) and will likely never write any native application of any significance using it (or any low-level language for that matter).
> Rust has been around for all 2-3 years now? 1.0 for what? Months?
Rust as a project has been around for eight years in total, about twoish in a similar fashion to what it is today. It's been 1.0 for three months.Sure, C++98 or C++03.
Consider a codebase that uses the "comment indicates ownership transfer" pattern everywhere. Would it improve the code quality if one developer suddenly introduced unique_ptr, std::move and rvalue references in just one place in the API, when every other call would still be using the old style?
To my taste, the mental overhead of having two different ownership patterns would outweigh the advantage of the new compiler checks (which in this case would be mostly theoretical, since the devs working on this project would presumably feel at home with the old pattern).