I membri devono essere distrutti frequentemente nell'ordine corretto. Poiché la creazione di un membro è in ordine di inoltro e la distruzione è in ordine inverso, di solito funziona bene. Tuttavia, quando sono coinvolti gli operatori di assegnazione e gli oggetti non copiabili, le cose possono iniziare a guastarsi, a causa dell'ordine di trasferimento in avanti nell'operatore di assegnazione.
Nell'esempio seguente, ho un oggetto simile al registro e oggetti che registrano / annullano la registrazione. A causa di questa dipendenza circolare, sia gli elementi che il registro sono non copiabili / non mobili e gli elementi devono essere distrutti prima che il registro venga distrutto.
Le istanze della classe holder
, possiedono un registry
e un item
tramite unique_ptr
s, in quanto il titolare stesso deve essere mobile.
Codice di esempio:
#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{});
}
}
Come si vede qui, l'ordine di distruzione viene violato durante l'assegnazione del movimento (non si blocca in questo esempio minimo per me, tuttavia il codice reale lo fa e anche questo, come funzione membro di un oggetto distrutto, è chiamato):
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
Aggiungendo quanto segue alla classe holder
, il problema è risolto
holder & operator=(holder && o) & noexcept
{
i = std::move(o.i);
r = std::move(o.r);
}
holder(holder && o) noexcept = default;
come si può vedere qui
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
Tuttavia, come ora definiamo il nostro operatore di assegnazione delle mosse, violiamo la regola dello zero (invocando la regola di cinque) e dobbiamo scrivere anche il nostro costruttore di mosse (in questo caso esplicitamente predefinito).
Esiste un modo più pulito (che non comporta la modifica del codice esterno) per garantire che l'ordine di distruzione venga mantenuto in uno scenario simile?