6

Members must frequently be destroyed in the correct order. As member creation is in forward order and destruction is in reverse order this will usually work fine. However, when assignment operators and non-copyable objects are involved, things can start to break down, due to the forward order of move-assignment in the assignment operator.

In the following example, I have a registry-like object and items that register/unregister themselves. Due to this circular dependency, both items and registry are non-copyable/non-movable, and items must be destroyed before the registry is destroyed.

Instances of the holder class, own a registry and an item via unique_ptrs, as the holder itself must be movable.

Example code:

#include <algorithm>
#include <iostream>
#include <memory>
#include <vector>

struct item;

/// registry that knows about items that register/unregister themselves on creation/destruction (stand-in for external code)
struct registry
{
    registry()
    {
        std::cout << " creating registry " << this << "\n";
    }

    ~registry()
    {
        std::cout << " destroying registry " << this << "\n";
    }

    void register_item(item * i)
    {
        is.emplace_back(i);
    }

    void unregister_item(item * i)
    {
        using std::begin; using std::end;
        is.erase(
            std::remove(begin(is), end(is), i),
            end(is)
        );
    }

private:
    std::vector<item *> is;
};

/// item that knows about a registry to which it is registered (stand-in for external code)
struct item
{
    explicit item(registry & r)
        : r{&r}
        , sth{0}
    {
        std::cout << " creating item " << this << " with registry " << &r << "\n";
        r.register_item(this);
    }

    ~item()
    {
        std::cout << " destroying item " << this << " and unregistering from " << r << "\n";
        r->unregister_item(this);
    }

    void do_something()
    {
        ++sth;
        std::cout << sth << "\n";
    }

private:
    registry * r;
    int sth;
};

/// class that owns a registry and one (or more) items
struct holder
{
    holder()
        : r{std::make_unique<registry>()}
        , i{std::make_unique<item>(*r)}
    {}

private:
    std::unique_ptr<registry> r;
    std::unique_ptr<item> i;
};

int main(int, char **)
{
    {
        // creation and destruction work, due to reverse order of destruction
        std::cout << "create/destroy\n";
        holder h;
    }
    {
        // move-assign fails due to forward order of assignment
        std::cout << "move-assign\n";
        holder h;
        h = std::move(holder{});
    }
}

As seen here, the order of destruction is violated during move-assignment (doesn't crash in this minimal example for me, however the real code does and this one could as well, as a member function of a destroyed object is called):

create/destroy
 creating registry 0xdc6f30
 creating item 0xdc6f50 with registry 0xdc6f30
 destroying item 0xdc6f50 and unregistering from 0xdc6f30
 destroying registry 0xdc6f30
move-assign
 creating registry 0xdc6f30
 creating item 0xdc6f50 with registry 0xdc6f30
 creating registry 0xdc1550
 creating item 0xdc1570 with registry 0xdc1550
 destroying registry 0xdc6f30
 destroying item 0xdc6f50 and unregistering from 0xdc6f30
 destroying item 0xdc1570 and unregistering from 0xdc1550
 destroying registry 0xdc1550

By adding the following to the holder class, the issue is resolved

holder & operator=(holder && o) & noexcept
{
    i = std::move(o.i);
    r = std::move(o.r);
}

holder(holder && o) noexcept = default;

as can be seen here

create/destroy
 creating registry 0x7a6f30
 creating item 0x7a6f50 with registry 0x7a6f30
 destroying item 0x7a6f50 and unregistering from 0x7a6f30
 destroying registry 0x7a6f30
move-assign
 creating registry 0x7a6f30
 creating item 0x7a6f50 with registry 0x7a6f30
 creating registry 0x7a1550
 creating item 0x7a1570 with registry 0x7a1550
 destroying item 0x7a6f50 and unregistering from 0x7a6f30
 destroying registry 0x7a6f30
 destroying item 0x7a1570 and unregistering from 0x7a1550
 destroying registry 0x7a1550

However, as we now define our own move assignment operator, we violate the rule of zero (invoking the rule of five) and must write our own move constructor as well (explicitly defaulted in this case).

Is there any cleaner way (that doesn't involve changing the external code) to ensure that the destruction order is maintained in such a scenario?

10
  • Making sure things get destructed properly takes priority over the order of destruction. Apr 20, 2016 at 14:33
  • 1
    To be destroyed properly they must be destroyed in the correct order
    – Joe
    Apr 20, 2016 at 14:34
  • Really? Is is just a rule, or is there some legitimate reason why things have to be disposed in a certain order? Do things break if you don't dispose in the correct order? In a garbage collected language, you never have to think about order of destruction. Objects would be collected when the number of references to them reaches zero, and I think you will find that's true in RAII as well. Apr 20, 2016 at 14:35
  • Yes, as pointed out in the question. In particular the lines destroying registry 0xdc6f30 and destroying item 0xdc6f50 and unregistering from 0xdc6f30. Also, I am specifically talking about C++, and not some garbage collected language.
    – Joe
    Apr 20, 2016 at 14:38
  • 1
    If the order of destruction matters then assume your design is bad, unless you can prove there's nothing better.
    – Dunk
    Apr 20, 2016 at 17:45

1 Answer 1

2

I think it is an excellent observation that default copy and move assignment do work in the opposite order of the destructor when "destroying" members. I think it's worth keeping in mind when looking for bugs in that area.

However,

Members must frequently be destroyed in the correct order. ...

I would disagree here. I would rather say, most of the time I don't care at all in what order my members are destructed! It's a brittle design to begin with, and I try to avoid it where possible. Obviously, there are cases where you cannot.

The way I see this is that IFF you need to rely on the stack-like ctor/dtor order of object members, then you build a holder_impl that is itself non-copy/non-moveable that holds the corresponding members (by whatever means) and this single object is then held by a holder through a unique_ptr.

This also ties in nicely with the Single Responsibility Principle:

  • One type to manage the allocation/deallocation in the correct order
  • Second type to manage the moveability of the first one.
2
  • I'm not sure if Single Responsibility applies here. Responsibility here is to manage resource life time. Oct 13, 2020 at 11:13
  • While the proposed solution works and is nice from the OOD point of view, in some cases it can cause CPU cache misses. When you access data members (registry or item) you have to dereference pointer first. pointer itself can be stored in another cache line than pointers to registry and item. That approach impair data locality. Oct 13, 2020 at 11:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.