Why does moving a pointer variable not set it to null?
Asked Answered
S

5

50

When implementing move constructors and move assignment operators, one often writes code like this:

p = other.p;
other.p = 0;

The implicitly defined move operations would be implemented with code like this:

p = std::move(other.p);

Which would be wrong, because moving a pointer variable does not set it to null. Why is that? Are there any cases were we would like the move operations to leave the original pointer variable unchanged?

Note: By "moving", I do not just mean the subexpression std::move(other.p), I mean the whole expression p = std::move(other.p). So, why is there no special language rule that says "If the right hand side of an assignment is a pointer xvalue, it is set to null after the assignment has taken place."?

Shogunate answered 26/2, 2012 at 10:55 Comment(18)
Why should there be? The only thing you're supposed to do with a 'moved' object is ignore it. Having a pointer point to memory not owned by the class shouldn't be a problem if you no longer use the pointer, right?Incunabula
"You don't pay for what you don't use"?Unequaled
@hvd: The destructor surely won't ignore it if it says delete p :)Shogunate
@FredOverflow The destructor surely will ignore it if it doesn't say delete p -- how does the compiler know? If you require the pointer to be set to null, set it to null, don't have the compiler do it for classes that don't need it.Incunabula
@hvd: If you write a destructor yourself, no move operations will automatically be generated by the compiler, and then there is nothing to discuss.Shogunate
@FredOverflow And an automatically generated destructor will never say delete p, so what is the point you're trying to make?Incunabula
@FredOverflow: The delete p is a bad argument, raw pointers should never be owning.Depredate
@hvd: I could simply say T& operator=(T&&) = default; and with my "proposal", it would do the right thing.Shogunate
@Depredate good point. Moving smart pointers does the right thing, so if only smart pointers own anything then moving raw pointers already does the right thing. If you have a pointer you're manually deleting, wrap it in unique_ptrLandholder
std::swap on two pointers is an excellent example of where you don't want std::move to auto-NULL pointers. And what is the "null" state for integers? True, an optimizer could solve the std::swap case to be ideal again, but I think such cases show that we better leave it alone.Undenominational
Wrapping your pointers in a wrapper-class whose move ctor and assignment op automatically nulls the pointer will make you able to use the implicit move ctor/assignment for containing classes.Undenominational
@JohannesSchaub-litb Could you elaborate on the std::swap issue? I'm not sure what you're getting at. Inefficiency due to unnecessary nulling out?Shogunate
Note: By "moving", I do not just mean the subexpression std::move(other.p) This is why std::move is a @!#^ing ridiculous name for that construct.Bibliogony
@Lightness Stroustrup says: "move(x) means "you can treat x as an rvalue". Maybe it would have been better if move() had been called rval(), but by now move() has been used for years."Shogunate
@FredOverflow: Yes, that would have been a much better name for it. Or get_rvalue(). Instead it's a "user-friendly" name, chosen because of its alleged most common use case, but since that only holds in conjunction with other language features (i.e. actually moving), I think it betrays the whole principle of C++. I'd accept the reasoning that it's too late to change (and by "it's" I mean during the late stages of C++11's standardisation, when I became aware of this calamity).Bibliogony
@FredOverflow If I "propose" that the compiler always calls delete on any pointer members in the automatically generated destructor, I could simply say ~T() = default; and it would do the right thing, wouldn't it? But well... ;)Diella
even rval would be incorrect, because rval(function) yields an lvalue.Undenominational
@BoPersson You are explicitly using it by with a move operation somewhere in the chain, so I think paying for it would be fine. Copying would retain its default, and less expensive behavior.Kolnos
B
39

Setting a raw pointer to null after moving it implies that the pointer represents ownership. However, lots of pointers are used to represent relationships. Moreover, for a long time it is recommended that ownership relations are represented differently than using a raw pointer. For example, the ownership relation you are referring to is represented by std::unique_ptr<T>. If you want the implicitly generated move operations take care of your ownership all you need to do is to use members which actually represent (and implement) the desired ownership behavior.

Also, the behavior of the generated move operations is consistent with what was done with the copy operations: they also don't make any ownership assumptions and don't do e.g. a deep copy if a pointer is copied. If you want this to happen you also need to create a suitable class encoding the relevant semantics.

Bionics answered 26/2, 2012 at 13:8 Comment(2)
I just remembered reading a paper that said "moving should never be more expensive than copying" or something like that. Setting the original pointer null would break that rule. Do you know which paper I'm talking about, or did my brain make that up?Shogunate
I'm not completely fine with the first sentence "Setting a raw pointer to null after moving it implies that the pointer represents ownership." Why is that? I disagree. In a modern code base heap ownership is handled by smart pointers. Raw pointers are merely equally to a reference, except that the developer has the option to reference to "nothing". In an old code base you do not know if a pointer is used for owning heap or just referencing something. Thus setting a pointer to null IMHO does not imply owning it.High
B
9

Moving renders the moved-from object "invalid". It does not automatically set it to a safe "empty" state. In accordance with C++'s long-standing principle of "don't pay for what you don't use", that's your job if you want it.

Bibliogony answered 26/2, 2012 at 14:46 Comment(0)
F
4

I think the answer is : implementing such a behavior yourself is pretty much trivial and hence the Standard didn't feel any need to impose any rule on the compiler itself. The C++ language is huge and not everything can be imagined before its use. Take for example, C++'s template. It was not first designed to be used the way it is used today (i.e it's metaprogramming capability). So I think, the Standard just gives the freedom, and didn't make any specific rule for std::move(other.p), following one of it's the design-principle: "You don't pay for what you don't use".

Although, std::unique_ptr is movable, though not copyable. So if you want pointer-semantic which is movable and copyable both, then here is one trivial implementation:

template<typename T>
struct movable_ptr
{
    T *pointer;
    movable_ptr(T *ptr=0) : pointer(ptr) {} 
    movable_ptr<T>& operator=(T *ptr) { pointer = ptr; return *this; }
    movable_ptr(movable_ptr<T> && other) 
    {
        pointer = other.pointer;
        other.pointer = 0;
    }
    movable_ptr<T>& operator=(movable_ptr<T> && other) 
    {
        pointer = other.pointer;
        other.pointer = 0;
        return *this;
    } 
    T* operator->() const { return pointer; }
    T& operator*() const { return *pointer; }

    movable_ptr(movable_ptr<T> const & other) = default;
    movable_ptr<T> & operator=(movable_ptr<T> const & other) = default;
};

Now you can write classes, without writing your own move-semantics:

struct T
{
   movable_ptr<A> aptr;
   movable_ptr<B> bptr;
   //...

   //and now you could simply say
   T(T&&) = default; 
   T& operator=(T&&) = default; 
};

Note that you still have to write copy-semantics and the destructor, as movable_ptr is not smart pointer.

Floro answered 26/2, 2012 at 11:1 Comment(8)
@Fred - It would if you used a sufficiently smart pointer instead of storing raw pointers. Could that be the problem?Unequaled
@BoPersson: I think that should be an answer?Floro
@BoPersson That and "You don't pay for what you don't use" would make an excellent answer.Shogunate
@FredOverflow: Do implicitly defined move operators call std::move?Floro
Move operations are simply defined to call the move operations of the members, see 12.8 §15.Shogunate
@FredOverflow: In that case, is this a compiler bug : ideone.com/tMqUo .. Or am I missing something? (I was expecting "moved (using move-assignment)" as output).Floro
12.8 §9 says that your expected output should be the correct one. ideone uses gcc-4.5.1, my best guess is that this version does not implement the rules correctly yet.Shogunate
@FredOverflow: Aah.. I tried it on my local PC, and it gave I was expecting. Anyway, I updated my answer, with my experiments.Floro
A
0

For example, if you have a pointer to a shared object. Remember, that after moving an object must remain in an internally consistent state, so setting a pointer that must not be null to a null value is not correct.

I.e.:

struct foo
{
  bar*  shared_factory;  // This can be the same for several 'foo's
                         // and must never null.
};

Edit

Here is a quote about MoveConstructibe from the standard:

T u = rv;
...
rv’s state is unspecified [ Note:rv must still meet the requirements
of the library component that is using it. The operations listed in
those requirements must work as specified whether rv has been moved
from or not.
Anastigmat answered 26/2, 2012 at 11:2 Comment(8)
But setting the pointer variable to null would only happen on xvalues. The client would not be able to inspect the null pointer afterwards.Shogunate
What if your destructor, for instance, uses the pointer somehow and will invoke UB if it is null? Also, I can't back up my claim with a quote from the standard, but as far as I remember, after moving object must remain valid (in that you can continue use it normally), not just destructable.Anastigmat
@FredOverflow: found relevant quote in the standard.Anastigmat
@doublep: The MoveConstructible requirement is only for the standard containers, nothing else. Also, you define the "internal consisten state" of your objects.Depredate
@Xeo: Even if so, it would be pretty weird if C++ stdlib contained features (nulling moved-out pointers) that'd go against its own requirements for other parts.Anastigmat
@Xeo: Also, you have to follow MoveConstructible requirements to make sure your objects work well with standard containers that require elements to be MoveConstructible, for example.Anastigmat
Can you explain why we cannot change your statement to "... so moving from a pointer that must not be null is not correct."?Undenominational
@Johannes Schaub: Default move constructor will move all the fields without paying attention to whether it is "correct" or not. Also, in template metaprogramming it is useful to treat all types the same, and assume that if a type has move constructor then it is movable (or else moving will implicitly fall back to copying).Anastigmat
H
0

I think what makes the difference here is a fully blown object on the one hand and a POD on the other hand.

  • For objects either the implementer specifies what move construction and move assignment should do or the compiler generates a default. The default is to call the move constructors/assignment operators of all member.
  • For POD's (and a pointer is a POD) C++ inherits from C and nothing is done with it when not explicitely coded. It's just the same behavior as POD members in a class are treated in a constructor. If you don't explicitely put them in the initializer list, well, then they stay uninitialized and are a source of potential bugs. AFAIK this is even valid for compiler generated constructors! That's why I took on the habit of generally initializing all members to be on the safe side.
High answered 3/7, 2019 at 14:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.