Boost logo

Boost :

From: brangdon_at_[hidden]
Date: 2002-03-10 12:36:18


In-Reply-To: <F59EBD80-33B1-11D6-BDFB-003065D18932_at_[hidden]>
On Sat, 9 Mar 2002 18:04:07 -0500 Howard Hinnant (hinnant_at_[hidden])
wrote:
> I know of no types of objects for which a non-destructive
> assign is not implementable (worst case scenario is a swap).

Well, provided assignment is supported at all, of course. A routine which
destroys and copy-constructs, or transfers and copy-constructs, is
arguably more general than one which moves and assigns. It can work for
immutable objects, eg for objects with const members. Not an issue for
std::vector, but perhaps worth bearing in mind generally.

I'm just being a bit pedantic. I agree with the rest of the summary (which
I've snipped).

> relocate construct is not implementable for those classes that have a
> base class or member classes that do not support move construct (in
> order to preserve proper order of construction/destruction of base and
> member objects).

OK, this is new to me.

How does move help with the order of construction/destruction here? What
is "proper order", anyway? At first sight, either construction order or
reverse construction order are equally applicable. Either can be wrong.

In practice, I think construction order should apply. This is because the
destructors are not actually called - the move/transfer constructor
includes the destruction and the object mustn't be destroyed twice.

Here is an example of a class with an order dependency between members.
Logger needs to log constructor/destruct events to a LogSink. LogSink must
own resources to log successfully.

    template <typename T>
    struct transfer_t { // Just to illustrate - details vary.
        T &value;
        explicit transfer_t( T &value_ ) : value(value_) {}
        T *operator->() { return &value; }
    };

    class Container {
        LogSink sink;
        Logger logger;
    public:
        Container() : sink( "filename" ), logger( &sink ) {}
        ~Container() {}
        
        Container( transfer_t<Container> &rhs ) :
                sink( transfer_t<LogSink>(rhs->sink) ),
                logger( transfer_t<Logger>(rhs->logger), &sink ) {}
    };

    class Logger {
        LogSink *pSink;
    public:
        Logger( LogSink *pSink_ ) : pSink(pSink_) {
            pSink->log( "created %p", this );
        }
        ~Logger() {
            pSink->log( "destroyed %p", this );
        }
        
        Logger( transfer_t<Logger> &rhs, LogSink *pSink_ ) :
                pSink(pSink_) {
            pSink->log( "destroyed %p", &rhs->value );
            pSink->log( "created %p", this );
        }
    };

The Logger transfer-constructor is responsible for logging two events
because it effects both construction and destruction. It has to take the
second LogSink argument because otherwise it has no way of knowing where
its new sink is. Therefore inside the body it has 2 sinks available, the
new one in pSink_ and the old one in rhs->pSink. At most one of these can
be valid. The order of transfer of members determines which one.

Do you see some way to use move() which allows the "destroyed" message to
be written to the old sink and the "created" message to be written to the
new sink? How can that be possible if resources for only one sink are
allocated?

I think we have a choice between writing both messages to the new sink or
both to the old. Writing both to the new sink is more natural and logical,
because it arises from the order of construction and a
transfer-constructor is a constructor. (It has to be a constructor because
it needs arguments and there is no way to pass arguments to destructors.)

All the above applies whether we are "moving" or "transferring".

> Another issue with relocate construct is that you can
> only do this with a heap object as the source. That's not a killer
> argument, but it does mean you have to be very careful. Essentially
> relocate construct must be handled with the same care as an explicit
> destructor call.

Good point. Very good point. It means relocate cannot be used with
temporary objects. That's a strong argument in favour of move. I'm almost
persuaded.

> 1. There must be a way to overload a function in such a way as to
> distinguish between a non-const lvalue, a const lvalue and a non-const
> rvalue as parameters.

Is this another approach to working with temporaries? I had thought the
issue would be addressed more directly by turning temporaries into lvalues
and allowing them to bind to non-const references (with implicit
conversions disallowed). This (and typeof) are the two most important
language changes I hope for.

If there is a better idea, I'd like to learn more. Probably comp.std.c++
would be a better forum, though.

-- Dave Harris


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