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.
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.
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.
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()? 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.
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. But yes, I do think that the fstream invariants are too weak. 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. 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. 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. Either you write ifs, or you use exceptions and (effectively) the compiler does it for you.