|
Boost : |
Subject: Re: [boost] [variant] Basic rvalue and C++11 features support
From: Paul Smith (pl.smith.mail_at_[hidden])
Date: 2013-01-08 11:35:15
On Tue, Jan 8, 2013 at 1:18 PM, Joel de Guzman <djowel_at_[hidden]> wrote:
> On 1/8/13 11:05 AM, Paul Smith wrote:
>>
>> On Tue, Jan 8, 2013 at 2:04 AM, Joel de Guzman <djowel_at_[hidden]> wrote:
>>>
>>> On 1/8/13 4:14 AM, Paul Smith wrote:
>>>>
>>>>
>>>> A recursive_wrapper is not a pointer. It's a value-like wrapper
>>>> that is assumed to always contain a valid object. The move
>>>> constructor should leave the moved-from recursive_wrapper
>>>> in a valid state, which precludes nullifying it.
>>>> That is, unless you suggest adding an "empty" state to
>>>> recursive_wrapper, which doesn't sound like a very good
>>>> idea.
>>>
>>>
>>>
>>> I disagree. That state will happen only when copying rvalues which
>>> will immediately be destructed anyway. What danger do you see in
>>> that situation? Example:
>>>
>>> recursive_wrapper<foo> bar() {...} // function returning
>>> recursive_wrapper
>>>
>>> recursive_wrapper<foo> foo(bar()); // copy
>>>
>>> Under no circumstances will anyone get to see that "empty" state.
>>> Do you see something that I don't?
>>>
>>> Without this move optimization (as it currently is), it is very
>>> inefficient
>>> especially with big structures (e.g. tuples and fusion adapted structs).
>>> Without this optimization, such temporary copies will end up with two
>>> heap allocations and unnecessary copying of the structures, instead of
>>> one heap allocation and a simple pointer swap. That would mean the missed
>>> optimization in the order of magnitudes with applications that use
>>> variant
>>> heavily (e.g. Spirit).
>>
>>
>> I hear ya. However, the C++11 take on move semantics is, for better
>> and for worse, a conservative one and not a destructive one. It's a
>> common misconception that a moved-from object is "as good as dead" and
>> that the move constructor can perform an autopsy. (It took me a while
>> to realize that, too).
>>
>> A move constructor has merely a "license to mutate" the source object
>> while keeping it's invariants, not a "license to kill". This is, I
>> believe, the intended convention. This is more or less what the STL
>> expect from client types. And, even though it's not enforced by the
>> core language, the effective semantics of rvalues and
>> rvalue-references are such that a destructive move is unsafe. In
>> particular, subobjects of rvalues are rvalues of the same value
>> category, and can equally well bind to rv-refs. Even if a top-level
>> rvalue is immediately destroyed after one of its subojects has been
>> moved-from, its destructor might still attempt to use that subobject.
>> If a type gives me public access to one of it's subobjects, it means
>> that I'm allowed to mutate it using it's public interface any way I
>> want, but nothing more. If the subobject's move consutrctor (which I
>> can rightfully invoke) does something that's impossible to acheive
>> using the public interface, I'm breaking this contract, and even if I
>> no longer use the containing object, I might get punished when it's
>> destructor takes place:
>>
>> class base {
>> void something_naughty(); // don't attempt to use the
>> // object after calling me
>> public:
>> base();
>> base(base&& other) {other.something_naughty();}
>>
>> void something_ordinary();
>> };
>>
>> struct derived : base {
>> ~derived() {something_ordinary();}
>> };
>>
>> int main() {
>> base b = derived(); // I'm only using the public interface
>> // of derived. How did I manage to
>> // screw something up?
>> }
>>
>> Note that this also applies to data members.
>> Also, note that rvalues can ofcourse be std::moved lvalues, in which
>> case they can be accessed by the programmer after having moved them,
>> and he might wish to reuse them (or, if the lvalue is an lv-ref
>> parameter, he might not know if whoever passed him this lvalue does),
>> even though, admittedly, I haven't seen many convincing examples of
>> such cases.
>>
>> I agree that the cases for which a destructive move is appropriate
>> seem by far the dominant ones (even if it's no different than a
>> conservative move in many of them). If you're asking for a rationale
>> as to why the semantics are the way they are, then you're talking to
>> the wrong person. I am sure David Abrahams can give a much more
>> credible answer than I can. Some of these issues are discussed in
>> N1377, in particular, see the "Alternative Move Designs" ->
>> "Destructive move semantics" section
>>
>> (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm#Alternative
>> move designs)
>> (Also note that the "Copy vs Move" section literally states: "The only
>> requirement (from the move-ctor PS) is that the (source PS) object
>> remain in a self consistent state (all internal invariants are still
>> intact).")
>>
>> FWIW, I'm playing with the idea of writing a destructive-move proposal
>> (although what I'm reffering to as destructive-move is a bit different
>> than what the move proposal calls a destructive-move), and these kind
>> of cases are exactly what I wish to address. If you have actual
>> numbers that demonstrate how bad is the impact on Spirirt and co, I'd
>> love to hear them.
>
>
> Those are very good points, but I think you are being too pedantic.
> The example you posted concerns a class hierarchy (base / derived).
> The paper you cited mentions:
>
> "When dealing with class hierarchies, destructive move semantics
> becomes problematic."
>
> That is certainly not the case with recursive_wrapper. Do you see
> a reasonable cause for danger with (a non-hierarchical struct such
> as) recursive_wrapper?
>
> Finally, it would be best if you quote from the standard. That paper
> you posted is quite old (from 2002).
>
>
> Regards,
> --
> Joel de Guzman
> http://www.boostpro.com
> http://boost-spirit.com
>
>
>
> _______________________________________________
> Unsubscribe & other changes:
> http://lists.boost.org/mailman/listinfo.cgi/boost
Not sure about being overly pedantic. I think I'm being appropriately
paranoid :)
This paper is indeed a bit old, but for the most part it's not
outdated. AFAIK the general concept of move semantics hasn't been
significantly changed since then, the only changes that underwent
concerned the actual core language technicalities (i.e. what rv-refs
can and can't bind to, implicitly declared move-ctors, etc'...)
It's a little hard to quote the standard here, simply because it
doesn't actually define the term "move semantics" - only the semantics
of the related core language facilities. As I said, I believe these
semantics imply a conservative move.
As for the library, see 17.6.3.1 [utility.arg.requirements] tables 20
[moveconstructible] and 22 [moveassignable].
The semantics of rvalues have similar effects on any kind of suboject:
struct X {
// the user is allowed to modify this member
recursive_wrapper<T> rw; // might as well be a
// recursive variant, I'm
// just being lazy.
X(): rw(...) {}
~X() {
if(predicate(rw.get()))
do_something();
else
do_something_else();
}
};
int main() {
recursive_wrapper<T> rw = X().rw; // oops
}
At the end of the day move semantics are mostly a convention. If we
agree on a conservative move, then it's legitimate for the programmer
to expect that moved-from objects remain valid. Period. And if we
don't, well... then we have a problem.
You don't have to convince me that in most cases a destructive move is
fine, and you don't have to convince me that in cases like this one
it's also desirable. I just think that the correct way to tackle this
is by extending the lanuage to naturally allow it and not by abusing
the current facilities.
-- Paul Smith
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk