Assegnazione di spostamento predefinita e ordine di distruzione dei membri rispetto alla regola dello zero

4

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?

    
posta Joe 20.04.2016 - 15:46
fonte

1 risposta

2

Penso che sia un'osservazione eccellente che i compiti di copia e spostamento predefiniti funzionano nell'ordine opposto del distruttore quando "distruggono" i membri. Penso che valga la pena tenere a mente quando cerchi bug in quella zona.

Tuttavia,

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

Non sarei d'accordo qui. Preferirei dire che la maggior parte delle volte non mi interessa in quale ordine i miei membri sono distrutti! Per cominciare è un progetto fragile, e cerco di evitarlo dove possibile. Ovviamente, ci sono casi in cui non è possibile.

Il modo in cui vedo questo è che IFF devi fare affidamento sull'ordine ctor / dtor stack-like dei membri dell'oggetto, quindi devi costruire un holder_impl che è esso stesso non-copy / non -moveable che contiene i membri corrispondenti (con qualsiasi mezzo) e questo singolo oggetto viene quindi trattenuto da un holder a un unique_ptr .

Questo si collega anche al Principio della singola responsabilità:

  • Un tipo per gestire l'allocazione / deallocazione nell'ordine corretto
  • Secondo tipo per gestire la movibilità del primo.
risposta data 12.01.2017 - 19:51
fonte

Leggi altre domande sui tag