Boost logo

Boost :

Subject: Re: [boost] [outcome] Exception safety guarantees
From: Andrzej Krzemienski (akrzemi1_at_[hidden])
Date: 2017-05-30 09:03:33


2017-05-30 4:13 GMT+02:00 Emil Dotchevski via Boost <boost_at_[hidden]>:

> 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.
>

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;


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk