2017-05-30 4:13 GMT+02:00 Emil Dotchevski via Boost
On Mon, May 29, 2017 at 12:50 AM, Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
Now, because I have a moved-from state, it weakens all my invariants. As you say, every function now has a precondition: either I put defensive if's everywhere, or expect the users to be putting them.
Indeed, and this is not a good thing. Consider that the int handle doesn't have move semantics, even though (in theory) it is possible to define some type with invalid "moved from" state, even in C.
When dealing with things like file handles or any other resourse it's best to use shared_ptr. This entire File class you wrote can be reduced to a function that returns shared_ptr<int const> that closes the file in a custom deleter. Things are even more straight-forward if you use FILE *, then it's just shared_ptr<FILE>.
Are you suggesting that one should use shared_ptrs instead of movable types? Are you saying that the design of a movable std::fstream is wrong?
Why is it better to use shared_ptr instead of move-only wrappers when dealing with (file) handles? Because handles are copyable types and (by design) they can be shared.
A raw file handle, (a pointer or an int) is indeed copyable and shareable. Agreed. But now, this is inherited from C, and is it because it was the optimal design choice, or is it only because in C you could not do it in a better way (e.g. by applying unique ownership)?
IMO unique_ptr is way overused and shared_ptr way underused. I know,
"Overhead!!",
Not only overhed. also the fact that you are implying shared ownership semantics (possibly across threads), even though you have none.
Your argument is that if it is incorrect to have more than one thread have access to the object then shared_ptr is the wrong design choice. But it doesn't necessarily follow, because move-only semantics don't _prevent_ multiple threads from accessing the object.
Agreed. In general, there is no way to prevent two threads from observing the same object. My point is that you can prevent it when you use value semantics. When I return shared_ptr-s even employing value semantics does no longer guarantee shared ownership, even though it could if I used a movable-only type. Besides, I don't think shared_ptr's move constructor does what you describe it does. From the Standard: shared_ptr(shared_ptr&& r) noexcept;
template<class Y> shared_ptr(shared_ptr<Y>&& r) noexcept;
Remarks: The second constructor shall not participate in overload resolution unless Y* is compatible with T*. Effects: Move constructs a shared_ptr instance from r. *Postconditions:* *this shall contain the old value of r. r shall be empty. r.get() == nullptr.
but like in the case of exception handling the overhead is
not a problem in general and it comes with the benefit of weak_ptr.
Maybe if you had a weak_ptr for a unique_ptr it would convince me.
You don't need weak_ptr for unique_ptr, because shared_ptr can do everything unique_ptr can do.
Yes, and far far more. And this is my objection. This type is doing far more than what I need, and therefore using it sends the wrong message to other developers (e.g., that I may be using shared ownership semantics. You already said that you can pass around naked handles anyway. But my view is that I want to wrap them in more constrained type to send a message to other developers and to compiler, that I will not be doing certain things, even though I know how to do them. This is my idea of safety: to constrain myself (with the choice of types) to do only the operations I require, and no more.
In practice many of the concerns you expressed can be dealt with if it's possible to hold on to the object just a bit longer until you're done with it. Using shared/weak_ptr replaces all of the defensive ifs you otherwise need to sprinkle around with a single if at lock time:
if( shared_ptr<foo> sp=wp.lock() ) { sp->do_a(); sp->do_b(); }
You also have one `if` here. I can also have a one-if solution with a unique_ptr:
``` if( unique_ptr<foo> sp = get() ) { sp->do_a(); sp->do_b(); } ```
It looks like the same (in)convenience to me.
I don't understand, what is get()?
I am just illustrating some function that returns a unique_ptr. Like a factory function.
The point I was making is that with shared_ptr, as long as you keep the shared_ptr afloat, the object isn't going anywhere and it is safe to use. Contrast this with an object which could have been moved-from and left in a not-quite-valid state.
You can also move from a shared_ptr, and it no longer owns the object. This is my understanding of the specification of std::shared_ptr.
Again, I understand your reasoning, but I think it equally well applies to
moved-from state. Are you also describing shortcommings of moves?
Not necessarily. Note that a "moved from" std::vector is still a good vector. I understand that sometimes it does make sense to define a less than completely valid state and the language does support that, but these should be treated as unfortunate necessities rather than good C++ programming practices, especially because they effectively butcher RAII.
What about std::fstream. Does it butcher RAII?
"move constructor: Acquires the contents of x. First, the function move-constructs both its base iostream class from x and a filebuf object from x's internal filebuf object, and then associates them by calling member set_rdbuf. x is left in an unspecified but valid state."
"Unspecified but valid state" tells me that there is nothing wrong with using a moved-from std::fstream.
Really, and will you put more characters to it with operator< I think you will only want to destroy it or rebind it to another file.
But yes, I do think that the fstream invariants are too weak.
And would you make it so strong that operator<< still works with well defined semantics? And does what?
I think this can also be dealt with one-if solution as you described.
``` if( outcome<foo> sp = foo::make() ) // factory instead constructor { sp->do_a(); sp->do_b(); } ```
Compare to:
shared_ptr<foo> x=foo::make(); x->do_a(); x->do_b();
The if is gone because foo::make won't return upon failure.
How come the if is gone? shared_ptr has a weak invariant: it can be a nullptr. I personally do not check shared_ptr-s returned from factories for null, but it is my understanding that your position is that you should always be sure you are not having the weak state.
Also this is repeated every time you call a function which may fail: when using exception handling, exception-neutral contexts don't have to worry about that possibility. If you don't use exceptions, you have to make sure you communicate the failure, which is prone to errors.
I absolutely agree: when you throw exceptions to communicate failures, and you do not catch them prematurely, you do not have to worry about defensive ifs. We seem to agree here. But this is exactly why I am in favor of having the valueless_by_exception state (if preventing it comes at efficiency cost): if you use exceptions correctly, you will never observe this state: only destructors will (or sometimes assignments).
More formally, exception handling allows you to enforce postconditions: the code that follows foo::make() requires that the object was created successfully. Therefore, the code that calls make() has to enforce the postcondition that make was successful.
I agree with you here.
Either you write ifs, or you use exceptions and (effectively) the compiler does it for you.
I agree. I never wanted to promote defensive if-s. My claim is, if you use exceptions correctly, and design functions with exception safety in mind. The minimum guarantee (destroy-and-reset only) is not worse than "valid but unspecified state". I have not yet been convinced to the contrary. Regards, &rzej;