Search code examples
c++treenestedmoveunique-ptr

std::move between unique_ptr and another that it owns


Consider some class/struct

struct Foo{
    int val = 0;
    std::unique_ptr<Foo> child_a = NULL;
    std::unique_ptr<Foo> child_b = NULL;
    Foo(int val_):val(val_){}
    ~Foo(){std::cout<<"Deleting foo "<<val<<std::endl;}
};

as you might construct in a doubly linked list/binary tree or similar.

Now, consider that in such a list we have several such Foos which point to each other in a tree like fashion e.g. through

std::unique_ptr<Foo> root = std::make_unique<Foo>(Foo(0));
root->child_a = std::make_unique<Foo>(Foo(1));
root->child_b = std::make_unique<Foo>(Foo(2));
root->child_a->child_a = std::make_unique<Foo>(Foo(3));
root->child_a->child_b = std::make_unique<Foo>(Foo(4));
root->child_b->child_a = std::make_unique<Foo>(Foo(5));
root->child_b->child_b = std::make_unique<Foo>(Foo(6));

In this context, if I write

root = NULL

then all the linked children get deleted and I get output that looks something like

Deleting foo 0
Deleting foo 1
Deleting foo 3
Deleting foo 4
Deleting foo 2
Deleting foo 5
Deleting foo 6

My question then, is what exactly is happening when I do something like this

root = std::move(root->child_a);

The output in such a situation looks like

Deleting foo 0
Deleting foo 2
Deleting foo 5
Deleting foo 6

leaving only the child_a branch in place of the original root as one might hope.

But looking at this I realise I'm not completely sure how the std::move works under the hood here, and the "expected behavior" really seems to be taking the self-referential move for granted. I had always broadly thought that a move like this

a=std::move(b);

functioned very roughly as

a = NULL;
b.release();
a.get() = b.get();

but of course this can't be right here, because the first NULL would destruct b before it could replace the a which has just been removed.

Instead I imagine something like this is happening

b.release();
c = b.get();
a = NULL;
a.get() = c;

such that b is moved into some new raw pointer c so that a can be deleted without interfering with the original b.

But it took a bit of thought an experimentation to try to figure out what is going on here, and I'm still not sure, which is a bit unnerving when a) reading code with such uses whilst b) the vast majority of tutorials I can find on unique_ptrs just don't mention what to expect when moving nested pointers to each other.

Can anyone elaborate what is actually happening here and perhaps point me to a good resource?


Solution

  • from cppreference

    std::unique_ptr<T,Deleter>::reset

    void reset( pointer ptr = pointer() ) noexcept;

    Given current_ptr, the pointer that was managed by *this, performs the following actions, in this order:

    1. Saves a copy of the current pointer old_ptr = current_ptr
    2. Overwrites the current pointer with the argument current_ptr = ptr
    3. If the old pointer was non-empty, deletes the previously managed object if(old_ptr) get_deleter()(old_ptr).

    std::unique_ptr<T,Deleter>::operator=

    unique_ptr& operator=( unique_ptr&& r ) noexcept;

    Move assignment operator. Transfers ownership from r to *this as if by calling reset(r.release()) followed by an assignment of get_deleter() from std::forward<Deleter>(r.get_deleter())