Topic: Moving cv-qualified objects
Author: Robert Powell<robertpwell@gmail.com>
Date: Sun, 3 Jun 2012 17:28:32 -0700 (PDT)
Raw View
Hi,
I find it a little surprising that 'std::move' doesn't strip away
cv-ness. Cv-ness only matters (other than minor technicalities) during
the lifetime of an object: both the constructor and the destructor
operate on a cv-less object. As moving is, logically, somewhat of an
early destruction, It seems only reasonable to expect that once an
object is moved, it's cv-ness no longer makes a difference. Why should
I care if someone steals resources from a const object once it has
been moved?
Here's an example where it bites:
#include<iostream>
#include<utility>
struct movable {
movable() {}
movable(const movable&) {std::cout<< "copy"<< std::endl;}
movable(movable&&) {std::cout<< "move"<< std::endl;}
};
struct naive {
movable m;
const movable cm;
naive() {}
naive(naive&& other):
m(std::move(other.m)),
cm(std::move(other.cm)) // Oops. Calls the copy-ctor.
{}
};
int main()
{
naive a;
naive b = std::move(a);
}
When moving 'a' to 'b', 'a.m' is being moved while 'a.cm' is being
copied I see no reason why this is desireable?
As for an implicit move-ctor (assuming 'naive' didn't explicitly
define one), it gets trickier.
Looking at 12.8 "Copying and moving class objects" (I'm reading off
N3242), this clause uses the term "to move an object" freely, but I
couldn't find where, if any, "moving an object" is formally defined.
This makes it hard to reason about, so I'm assuming that, for the sake
of that clause, "to move an object" means to direct-initialize some
other object by an expression that is semantically equivalent to
'std::move(object)', AND having this get dispatched to a
move-constructor (i.e. and not a copy-constructor). With this reading,
the implicit move-ctor of 'naive' is implicitly deleted, because 'cm'
is not movable (the copy-ctor gets called), and it's copy-ctor is
non-trivial. So, that's even wrose than before. Now both 'm' and 'cm'
get copied!
I only have access to one compiler that implements implicit move-ctors
(GCC 4.6.1), and this is indeed what happens. If the copy-ctor of
'movable' is defaulted, then 'm' gets moved, and 'cm' doesn't.
I tried to think of a possible rationale for this. The only thing that
occured to me was that, maybe, it's a better-safe-than-sorry measure?
If a member is 'const', then the destructor might expect it to
maintain it's original value, in which case moving it is unsafe. But
this isn't very convincing. If the user defines a destructor, then no
implicit move-ctor is generated, so, moving has to be done explicitly.
In this case, if something shouldn't be moved, then the user just
shouldn't move it. I don't see how const-ness makes it much different
from anything else.
Is this behavior deliberate, or has this been overlooked?
(Btw, what's with the rule that an implicit move-ctor is deleted if
one of the members/bases is non-movable and has a non-trivial copy
(12.8.12)? If some members are movable and others aren't, wouldn't the
most obvious move-ctor move what it can, and copy the rest? Why should
one pay for a full copy?)
--
Robert P.
[ comp.std.c++ is moderated. To submit articles, try posting with your ]
[ newsreader. If that fails, use mailto:std-cpp-submit@vandevoorde.com ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: Jason McKesson <jmckesson@gmail.com>
Date: Mon, 4 Jun 2012 17:11:58 -0700 (PDT)
Raw View
On Sunday, June 3, 2012 5:28:32 PM UTC-7, Robert Powell wrote:
> Hi,
>
> I find it a little surprising that 'std::move' doesn't strip away
> cv-ness. Cv-ness only matters (other than minor technicalities) during
> the lifetime of an object: both the constructor and the destructor
> operate on a cv-less object. As moving is, logically, somewhat of an
> early destruction, It seems only reasonable to expect that once an
> object is moved, it's cv-ness no longer makes a difference. Why should
> I care if someone steals resources from a const object once it has
> been moved?
Because it's *const*.
If I give you a `const&` to something, that's a contract: you can't
mess with this. You can't delete it (because it's a reference). You
can't alter its state. You can't call any member function designated
`const`, nor can you pass it to any other function that doesn't take
the value as a `const`.
You can *copy* it, because copying is non-destructive. However, by
definition, movement is *destructive*. It isn't freeing the object,
because the object still exists after the move. It still has a state,
and you can still talk to the object.
Movement is not "early destruction" and it never has been. It's simply
mutating the object. It is no more "early destruction" than doing
this:
std::swap(moveable(), m);
where `m` is some object of `moveable` type. You can't swap `const`
objects because swapping them is modifying them.
> Is this behavior deliberate, or has this been overlooked?
Yes, it is very deliberate.
> (Btw, what's with the rule that an implicit move-ctor is deleted if
> one of the members/bases is non-movable and has a non-trivial copy
> (12.8.12)? If some members are movable and others aren't, wouldn't the
> most obvious move-ctor move what it can, and copy the rest? Why should
> one pay for a full copy?)
It doesn't *force* you to pay for a full copy, because you can still
implement the move constructor yourself. Indeed, you can simply
declare it as `= default` if you want that behavior. The point is
that, because your class has non-moveable types in it, you must
*explicitly* state what you want. That way, if you accidentally put
non-moveable types into the object, you get a compiler error when you
try to move it.
It's just a way of the compiler forcing you to explicitly state, "Yes,
I'm sure that half-move/half-copy is what I *really* wanted."
--
[ comp.std.c++ is moderated. To submit articles, try posting with your ]
[ newsreader. If that fails, use mailto:std-cpp-submit@vandevoorde.com ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: =?ISO-8859-1?Q?Daniel_Kr=FCgler?= <daniel.kruegler@googlemail.com>
Date: Mon, 4 Jun 2012 17:13:02 -0700 (PDT)
Raw View
On 2012-06-04 02:28, Robert Powell wrote:
>
> I find it a little surprising that 'std::move' doesn't strip away
> cv-ness. Cv-ness only matters (other than minor technicalities) during
> the lifetime of an object: both the constructor and the destructor
> operate on a cv-less object. As moving is, logically, somewhat of an
> early destruction, It seems only reasonable to expect that once an
> object is moved, it's cv-ness no longer makes a difference.
Well "somewhat of an early destruction" is only a higher-level
description, it does not satisfy the constraints of the core language,
which still do not allow to modify a const object - whether you move
it or not. There is no requirement that a moved-from object is no
longer usable. But you as a user can impose such a constraints on
*your* types. E.g. for library-provided types a moved-from object can
be queried by all functions that do not have preconditions. E.g. you
may call the empty() or size() function from any moved-from
std::vector<T> instance.
Further, your description is not really true. Are you aware of a
compiler that accepts the following code:
#include <string>
struct X {
const std::string cs;
X() : cs("Hi") {}
~X() { cs[0] = 'F'; }
};
?
There is no modification of const members allowed in the destructor of
such a class.
> Why should
> I care if someone steals resources from a const object once it has
> been moved?
It may care, if the move-source was indeed a const object: Moving
objects does intentionally not break const-correctness. If you think
that is OK, you have to apply the const_cast explicitly.
> Here's an example where it bites:
>
> #include<iostream>
> #include<utility>
>
> struct movable {
> movable() {}
> movable(const movable&) {std::cout<< "copy"<< std::endl;}
> movable(movable&&) {std::cout<< "move"<< std::endl;}
> };
>
> struct naive {
> movable m;
> const movable cm;
>
> naive() {}
> naive(naive&& other):
> m(std::move(other.m)),
> cm(std::move(other.cm)) // Oops. Calls the copy-ctor.
> {}
> };
>
> int main()
> {
> naive a;
> naive b = std::move(a);
> }
>
> When moving 'a' to 'b', 'a.m' is being moved while 'a.cm' is being
> copied I see no reason why this is desireable?
I think it is very desirable: I would strongly assume a compiler
error, if the compiler would perform an implicit const-cast on member
cm in the move constructor. If you expect naive to be moveable, you
should not mark potentially moveable members that const members.
> As for an implicit move-ctor (assuming 'naive' didn't explicitly
> define one), it gets trickier.
> Looking at 12.8 "Copying and moving class objects" (I'm reading off
> N3242), this clause uses the term "to move an object" freely, but I
> couldn't find where, if any, "moving an object" is formally defined.
>
>
> This makes it hard to reason about, so I'm assuming that, for the sake
> of that clause, "to move an object" means to direct-initialize some
> other object by an expression that is semantically equivalent to
> 'std::move(object)', AND having this get dispatched to a
> move-constructor (i.e. and not a copy-constructor). With this reading,
> the implicit move-ctor of 'naive' is implicitly deleted, because 'cm'
> is not movable (the copy-ctor gets called), and it's copy-ctor is
> non-trivial.
Even though the term move or copy are defined, the core language
defines the definition of the implicitly generated copy constructor or
move constructor (similarly the corresponding assignment operators).
See 12.8 p15 in N3376.
> So, that's even wrose than before. Now both 'm' and 'cm'
> get copied!
Why is that worse? This is what I would expect to happen.
> I only have access to one compiler that implements implicit move-ctors
> (GCC 4.6.1), and this is indeed what happens. If the copy-ctor of
> 'movable' is defaulted, then 'm' gets moved, and 'cm' doesn't.
Correct, otherwise the compiler would break const-correctness. If you
think that this would be OK (But I can only recommend to stay away
from such an assumption without marking all non-static members as
mutable), you have to apply the const-cast by your-self.
Move-semantics was not invented to break const-correctness, it was
invented to allow implicit data transfer from non-const rvalues. This
also has the effect that you better should not apply const-qualifiers
to return types as in
const std::string get_my_string();
because a function call of get_my_string won't allow to move the
returned object.
> I tried to think of a possible rationale for this. The only thing that
> occured to me was that, maybe, it's a better-safe-than-sorry measure?
It was very important to realize that move-semantics does not conflict
with the existing language-safety, yes.
> If a member is 'const', then the destructor might expect it to
> maintain it's original value, in which case moving it is unsafe. But
> this isn't very convincing. If the user defines a destructor, then no
> implicit move-ctor is generated, so, moving has to be done explicitly.
> In this case, if something shouldn't be moved, then the user just
> shouldn't move it. I don't see how const-ness makes it much different
> from anything else.
"Moveing" is a mutable operation, it is not a destructible operation,
otherwise it would be ruled by the language rules of destroyed
objects. It is important to realize that moveing an object does not
end its life-cycle. Well, you *can* define moving your own objects
that way but I expect that such objects are not useable for most
generic code.
> Is this behavior deliberate, or has this been overlooked?
This was not overlooked.
> (Btw, what's with the rule that an implicit move-ctor is deleted if
> one of the members/bases is non-movable and has a non-trivial copy
> (12.8.12)? If some members are movable and others aren't, wouldn't the
> most obvious move-ctor move what it can, and copy the rest? Why should
> one pay for a full copy?)
Yes, this rule is a defect. There is an issue that attempts to solve
this problem, see
http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1402
The current P/R could be further improved and I hope it will be so.
HTH & Greetings from Bremen,
Daniel Kr gler
--
[ comp.std.c++ is moderated. To submit articles, try posting with your ]
[ newsreader. If that fails, use mailto:std-cpp-submit@vandevoorde.com ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: robertpwell@gmail.com
Date: Mon, 4 Jun 2012 19:11:23 CST
Raw View
On Monday, June 4, 2012 3:28:32 AM UTC+3, Robert Powell wrote:
> <snip>
> This makes it hard to reason about, so I'm assuming that, for the sake
> of that clause, "to move an object" means to direct-initialize some
> other object by an expression that is semantically equivalent to
> 'std::move(object)'
This is a mistake, ofcourse, it should have been
'std::forward<decltype(object)>(object)'. Which brings another issue.
In the library definition, 'forward' is frequently used not only to
forward parameters, but also to conditionally move objects that aren't
lv-refs (i.e. in the move-ctor of 'tuple' or 'pair'). These are two
different uses, conceptually, and I'm not sure whether this is
intended to be seen this as a kind of forwarding, or is it merely
terseness. But if we agree that 'move' should strip cv-qulifiers, then
surely so does this. Which means, departing from 'forward' in these
cases (possibly, in favor of a dedicated 'try_move' function, or
something similiar), as 'forward' should definitely preserve
cv-qualifiers.
Jumping back to the top, defining "moving an object" in terms of
'forward' was the reading of the current standard. Utlimately, this
too should be replaced with 'try_move'.
> <snip>
>
> (Btw, what's with the rule that an implicit move-ctor is deleted if
> one of the members/bases is non-movable and has a non-trivial copy
> (12.8.12)? If some members are movable and others aren't, wouldn't the
> most obvious move-ctor move what it can, and copy the rest? Why should
> one pay for a full copy?)
I just figured that the consequences 12.8/12 are far more severe,
definitely when combined with the cv-ness issue. It seems to be
out-of-sync with other parts of the standard.
For example, 'std::pair' and 'std::tuple' both declare a defaulted
move-ctor. This means, that if one of their elements is non-movable
(in the 12.8/12 sense) the instanciation of their move-ctor is
ill-formed. This contradicts the requirements of these functions,
namely that all element types satisfy 'is_move_construbtible<T>::value
== true', which is a weaker (and more appropriate) condition. Now,
since const objects aren't movable, you're in for trouble if any of
the elements is const. 'std::map' rings a bell?
The libstdc++ folks must have noticed that, as someone left a comment
above pair's move-ctor: "Defaulted?!? Breaks std::map!!!"
--
Robert P.
[ comp.std.c++ is moderated. To submit articles, try posting with your ]
[ newsreader. If that fails, use mailto:std-cpp-submit@vandevoorde.com ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]