Boost logo

Boost :

From: David Abrahams (dave_at_[hidden])
Date: 2003-01-29 22:14:13


"David B. Held" <dheld_at_[hidden]> writes:

> "David Abrahams" <dave_at_[hidden]> wrote in message
> news:ud6mfcv9c.fsf_at_boost-consulting.com...
>> [...]
>> It sure does if any of the bases or members of smart_ptr throws
>> from its constructor.
>
> Just when I thought the problem was solved...
>
>> [...]
>> I dunno. Acquiring ownership at construction time is a key part
>> of the "one true meaning" of RAII (not the accepted meaning,
>> which has come to be "deallocating resources in destructors" -- a
>> concept having nothing to do with acquisition _or_ initialization).
>> There's a good reason for this, since it avoids the problem of
>> leaking when a constructor throws.
>
> On the other hand, acquiring multiple resources is going to present
> exactly the same problem.

Which is why the first "I" in RAII stands for "is". Each acquired
resource should initialize exactly one (sub)object.

> Or acquiring a resource in any other context when members can throw
> will have the same problem.

I can't picture what you mean.

> You could say that no class should acquire a resource and have other
> members/bases that could throw

I don't think I'd say that.

> but that seems draconian. So what is the real solution?

To this particular problem? I'd say "redesign or respecify". That
is, move the design or move the goalposts.

> I'm beginning to agree with Andrei that there appears to be a
> fundamental language problem here, but I don't pretend to have a
> change that would fix it.

It's very easy to blame the language for not allowing us to easily
implement DWIM semantics, especially when we're not sure exactly what
those semantics should be. Have you got a clear idea of what should
happen during construction if an exception is thrown? Can you write
it as pseudocode?

The language gives us a very coherent mechanism for dividing up the
units of work that must get done when a constructor throws an
exception. See page 53 of "more effective C++"
(http://www.awprofessional.com/meyerscddemo/DEMO/MEC/MI10_FR.HTM) and
my 5/5/99 notes at
http://www.aristeia.com/BookErrata/mec++-errata_frames.html for
reference. Any "fix" to the language will only be an improvement if
it doesn't undermine that coherence.

> Modifying function try blocks to be more useful might seem like the
> answer, but any change would make them different from normal try
> blocks, right?

They're already different. But anyway, I think you're putting the
cart before the horse. First describe exactly what you think ought to
happen; then we can see how close or far the language is from allowing
it.

> Look at shared_count. It does not acquire the count in the initializer
> list. It default-constructs and then acquires the count inside a try
> block in the body of the c'tor. Does that mean it does not perform
> "True RAII"?

It's certainly walking the line. Internally it has to do something
other than pure RAII: all these smart pointer classes are complicated
by the fact that we expect them to take posession of an
already-allocated yet unmanaged resource in their constructors. If
they have to allocate any other resources before managing the one
they're passed, they have to take extraordinary measures.

One language change I could imagine which would help would be the
ability to declare an auto variable in the class' initializer list:

    template<class P, class D> shared_count(P p, D d)
       : auto deleter<P,D> del(d, p)
       , pi_(new sp_counted_base_impl<P, D>(p, d))
    {
       del.release();
    }

...and I suppose you could simulate this by adding an extra default
parameter:

    template<class P, class D> shared_count(
           P p, D d, deleter<P,D> del = deleter<P,D>(p,d))
       : pi_(new sp_counted_base_impl<P, D>(p, d))
    {
       del.release();
    }

So, does that help with your problem?

> I would like to use the initializer list as much as the next guy,
> but multiple initializers seem to be a real problem. It works great
> for the trivial case. Does this mean "True RAII" doesn't scale?

It scales fine; it's unmanaged resources which don't scale ;-)

> And this is an RAII problem, not a PBD or orthogonality problem.

That's still not totally clear to me. shared_count works fine from
the POV of its caller; you could make your policies do the same if
they were organized appropriately for that job (which may destroy
other nice properties like orthogonality).

> That's because as much as we would like to say that every class
> should only perform one function, it happens that some functions
> cannot be performed with a single resource.

That's no problem:

    struct totally_safe
    {
        totally_safe() : pa(new A), pb(new B), pc(new C) {}

        ...

        std::auto_ptr<A> pa;
        std::auto_ptr<B> pb;
        std::auto_ptr<C> pc;
    };

> For ref-counted smart pointers (shared_ptr or smart_ptr), those
> resources are the managed resource and the count.

Uh-huh.

> A kludge, but perhaps a start in the right direction, would be to have
> a special function analogous to the destructor that gets called when
> a member is destroyed as the result of a failed construction. It would
> be analogous to placement delete. It would only get called on fully
> constructed objects. This way, objects would be able to tell when
> they are being destroyed as part of their normal life cycle, and when
> they are being destroyed due to a problem.

Can you say, "std::uncaught_exception()"? That also has limited
usefulness, but again I think you're putting the cart before the
horse. What behavior do you want to implement?

> One syntax would be to pass a dummy int parameter to a normal
> destructor, like so:
>
> scalar_storage::scalar_storage()
> {
> // normal destruction, do nothing
> }
>
> scalar_storage::~scalar_storage(int)
> {
> // something bad happened, clean up with extreme prejudice
> destroy();
> }

That would mean the usual destructor wouldn't be called in case of an
exception?

> Another syntax might be to pass the exception type to the destructor.
> Then, a given destructor would only get called if its argument were
> convertible to the thrown exception type, like so:
>
> scalar_storage::~scalar_storage(std::bad_alloc const&)
> {
> // failed allocation somewhere else, clean up
> destroy();
> }
>
> scalar_storage::~scalar_storage(std::exception const&)
> {
> // some other problem, clean up anyway ;)
> destroy();
> }

Yikes! Very complicated! Probably misguided, as most things which
distinguish different exception types near the throw point tend to be.

> In the event that no specialized d'tor were defined, the normal
> destructor would get called.
>
> So, in this case:
>
> int main()
> {
> ref_counted r;
> }
>
> At the end of scope, ref_counted::~ref_counted() is called. Whereas,
> in this case:
>
> struct X
> {
> struct Y { Y() { throw 0; } };
>
> X() : r(), y() { }
>
> ref_counted r;
> Y y;
> };
>
> ref_counted::~ref_counted(int) would get called.

And if that weren't defined?

> Unfortunately, shared_count shows that this scheme is not
> sufficient, because you lose the context of the throwing c'tor. In
> particular, the shared_count(P, D) c'tor is most problematic. On
> the other hand, shared_count could technically be written using a
> function try block. Who knows. Maybe someone can come up with a
> better idea.

How about the default argument?

-- 
                       David Abrahams
   dave_at_[hidden] * http://www.boost-consulting.com
Boost support, enhancements, training, and commercial distribution

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