|
Boost : |
Subject: Re: [boost] [outcome] Exception safety guarantees
From: Emil Dotchevski (emildotchevski_at_[hidden])
Date: 2017-05-30 02:13:10
On Mon, May 29, 2017 at 12:50 AM, Andrzej Krzemienski via Boost <
boost_at_[hidden]> 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.
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk