Hacker News new | past | comments | ask | show | jobs | submit login

Let me make it a bit more concrete. I have a hazard pointer class, where the constructor registers the provided pointer for GC protection, and the destructor removes GC protection. I would like to be able to dereference this hazard pointer object freely, without doing null checks everywhere. RAII is the perfect fit for these semantics: the constructor establishes the invariant (GC-protected non-null pointer) and all other code can assume the invariant. Until I needed move semantics, that is.

In the constructor of a class with a hazard pointer member I needed to be able to initialize a hazard pointer on the stack and then move it into the member variable. (Because the hazard pointer constructor is fallible, I needed to catch exceptions thrown from the hazard pointer's constructor and retry from within the containing class's constructor, so I couldn't just use an initializer list.) In order to support move semantics, I had to give up the invariant that any hazard pointer instance is properly initialized (I needed to use a null pointer to represent the invalid state). That complicated all the clients, which now had to either check for or assert against the invalid state.

None of these gymnastics would have been necessary in Rust. Sure, it doesn't have constructors, but it's easy enough to write a factory method that establishes constructor invariants, and then you know that any object returned from that factory method will satisfy those invariants for the entire lifetime of the object. Since it is impossible to accidentally use a moved-from object (unlike C++), there is no need to introduce an invalid state to prevent misuse. I could just freely dereference my hazard pointers, with no checks or asserts necessary.




It seems like the obvious answer is to have a nullable_hazard_ptr and a hazard_ptr, which composites nullable_hazard_ptr. nullable_hazard_ptr is movable and default-constructible, while hazard_ptr can only be constructed with arguments and cannot be moved, but can be constructed from a nullable_hazard_ptr &&.

So if you need to return a hazard pointer from a function you return a nullable_hazard_ptr and the caller can choose to assigned that value to auto or to hazard_ptr. In the latter case, the caller will have the guarantee that the object is valid because if the function returned a null pointer the constructor will have thrown an exception. Furthermore the pointer will remain valid until it goes out of scope because there's nothing that can be done to it to make it invalid (UB notwithstanding). Of course, anyone who chooses to use nullable_hazard_ptr will need to check for validity.

Unfortunately this does mean that it's the responsibility of the callers to choose the right pointer.

>In the constructor of a class with a hazard pointer member I needed to be able to initialize a hazard pointer on the stack and then move it into the member variable. (Because the hazard pointer constructor is fallible, I needed to catch exceptions thrown from the hazard pointer's constructor and retry from within the containing class's constructor, so I couldn't just use an initializer list.)

This particular case would be handled by calling a helper function in the constructor's initialization list for each hazard_ptr member. As I said, this function should return nullable_hazard_ptr (always non-null; you will have already ensured this inside the function. You still need the nullable type because it's the only one that can be moved).

Ultimately what you have is something analogous to std::lock_guard and std::unique_lock. You are acquiring and releasing a resource and in some cases you need to tie the acquisition into the program structure and in other cases you need to be able untie it. There's no way to specify that in C++'s type system other than by having two separate types.


Thanks for this instructive example.


Here's a much simpler example of something impossible in C++ and trivial in Rust: how about a non-nullable unique_ptr? The constructor should just be able to check for null and then no code need ever check for null again, right? Sorry, you need an invalid state to represent a moved-from instance, so this is impossible.

Are you telling me that having to accommodate invalid states that are semantically both unnecessary and undesirable is not a serious limitation of C++ move semantics?


Following the previous example, you wrap std::unique_ptr in another class that has no move constructor and forwards constructor parameters to std::make_unique(), and can also be constructed from an std::unique_ptr. Now you have a heap-allocated smart pointer class that can't possibly be null.

Alternatively, you make it movable, and if someone tries to call operator*(), operator->(), or get() on the null value, you throw an exception. Not as clean, but, hey, it's safe.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: