c++ - Qual è la regola del tre?




copy-constructor assignment-operator (6)

Quando devo dichiararli da solo?

La Regola dei Tre afferma che se dichiari qualcuno di a

  1. copia costruttore
  2. copia l'operatore di assegnazione
  3. distruttore

allora dovresti dichiararli tutti e tre. Nacque dall'osservazione che la necessità di assumere il significato di un'operazione di copia derivava quasi sempre dalla classe che eseguiva un qualche tipo di gestione delle risorse, e che quasi sempre implicava che

  • qualunque sia stata la gestione delle risorse eseguita in un'operazione di copia probabilmente doveva essere eseguita nell'altra operazione di copia e

  • il distruttore di classe parteciperebbe anche alla gestione della risorsa (solitamente rilasciandola). La risorsa classica da gestire era la memoria, ed è per questo che tutte le classi della libreria standard che gestiscono la memoria (ad esempio i contenitori STL che eseguono la gestione dinamica della memoria) dichiarano tutte "le tre grandi": sia le operazioni di copia che un distruttore.

Una conseguenza della Regola del Tre è che la presenza di un distruttore dichiarato dall'utente indica che è improbabile che una semplice copia di membro sia appropriata per le operazioni di copia nella classe. Questo, a sua volta, suggerisce che se una classe dichiara un distruttore, le operazioni di copia probabilmente non dovrebbero essere generate automaticamente, perché non farebbero la cosa giusta. Al momento dell'adozione del C ++ 98, l'importanza di questa linea di ragionamento non era pienamente apprezzata, quindi in C ++ 98 l'esistenza di un distruttore dichiarato dall'utente non aveva alcun impatto sulla volontà dei compilatori di generare operazioni di copia. Questo continua ad essere il caso in C ++ 11, ma solo perché la limitazione delle condizioni in cui vengono generate le operazioni di copia causerebbe una rottura eccessiva del codice legacy.

Come posso impedire che i miei oggetti vengano copiati?

Dichiarare il costruttore di copia e l'operatore di assegnazione copia come specificatore di accesso privato.

class MemoryBlock
{
public:

//code here

private:
MemoryBlock(const MemoryBlock& other)
{
   cout<<"copy constructor"<<endl;
}

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
 return *this;
}
};

int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

In C ++ 11 in poi puoi anche dichiarare il costruttore di copia e l'operatore di assegnazione cancellati

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}
  • Cosa significa copiare un oggetto ?
  • Quali sono il costruttore di copie e l' operatore di assegnazione delle copie ?
  • Quando devo dichiararli da solo?
  • Come posso impedire che i miei oggetti vengano copiati?

introduzione

C ++ tratta le variabili dei tipi definiti dall'utente con la semantica del valore . Ciò significa che gli oggetti vengono copiati in modo implicito in vari contesti e dovremmo capire cosa significa "copiare un oggetto".

Consideriamo un semplice esempio:

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}

(Se sei perplesso dal name(name), age(age) parte, questo è chiamato un elenco di inizializzazione dei membri .)

Funzioni membro speciali

Cosa significa copiare un oggetto person ? La funzione main mostra due diversi scenari di copia. La person b(a); inizializzazione person b(a); viene eseguito dal costruttore di copie . Il suo compito è costruire un nuovo oggetto basato sullo stato di un oggetto esistente. L'assegnazione b = a viene eseguita dall'operatore di assegnazione copia . Il suo lavoro è generalmente un po 'più complicato, perché l'oggetto di destinazione è già in uno stato valido che deve essere affrontato.

Dal momento che non abbiamo dichiarato né il costruttore di copie né l'operatore di assegnazione (né il distruttore), questi sono definiti implicitamente per noi. Citazione dallo standard:

Il costruttore [...] copia e l'operatore di assegnazione [...] copia e il distruttore sono funzioni membro speciali. [ Nota : l'implementazione dichiarerà implicitamente queste funzioni membro per alcuni tipi di classi quando il programma non le dichiara esplicitamente. L'implementazione li definirà implicitamente se vengono utilizzati. [...] nota finale ] [n3126.pdf sezione 12 §1]

Per impostazione predefinita, copiare un oggetto significa copiare i suoi membri:

Il costruttore di copie implicitamente definito per una classe non unione X esegue una copia membro dei suoi sottooggetti. [n3126.pdf sezione 12.8 §16]

L'operatore di assegnazione delle copie implicitamente definito per una classe non unione X esegue l'assegnazione della copia membro a senso di senso dei suoi sottooggetti. [n3126.pdf sezione 12.8 §30]

Definizioni implicite

Le funzioni di membro speciale implicitamente definite per la person assomigliano a questo:

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}

La copia membro è esattamente ciò che vogliamo in questo caso: il name e l' age vengono copiati, quindi otteniamo un oggetto autonomo e indipendente. Il distruttore implicitamente definito è sempre vuoto. Anche in questo caso va bene, poiché non abbiamo acquisito risorse nel costruttore. I distruttori dei membri sono implicitamente chiamati dopo che il distruttore person è finito:

Dopo aver eseguito il body del distruttore e distrutto qualsiasi oggetto automatico allocato all'interno del corpo, un distruttore per la classe X chiama i distruttori per i membri [...] diretti di X [n3126.pdf 12.4 §6]

Gestione delle risorse

Quindi, quando dovremmo dichiarare esplicitamente quelle funzioni dei membri speciali? Quando la nostra classe gestisce una risorsa , cioè quando un oggetto della classe è responsabile di quella risorsa. Questo di solito significa che la risorsa viene acquisita nel costruttore (o passata nel costruttore) e rilasciata nel distruttore.

Torniamo indietro nel tempo al C ++ pre-standard. Non esisteva nulla come std::string , ei programmatori erano innamorati dei puntatori. La classe person potrebbe essere simile a questa:

class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};

Ancora oggi, le persone scrivono ancora classi in questo stile e si mettono nei guai: " Ho spinto una persona in un vettore e ora ho degli errori di memoria pazzi! " Ricorda che per impostazione predefinita, copiare un oggetto significa copiare i suoi membri, ma copiare il membro del name copia semplicemente un puntatore, non l'array di caratteri a cui punta! Questo ha diversi effetti spiacevoli:

  1. Le modifiche tramite a possono essere osservate tramite b .
  2. Una volta che b viene distrutta, a.name è un puntatore a.name .
  3. Se a viene distrutto, l'eliminazione del puntatore ciondolante produce un comportamento indefinito .
  4. Dal momento che l'incarico non tiene conto del name indicato prima dell'assegnazione, prima o poi si otterranno perdite di memoria ovunque.

Definizioni esplicite

Poiché la copia membrowise non ha l'effetto desiderato, dobbiamo definire esplicitamente il costruttore copia e l'operatore di assegnazione copia per creare copie profonde dell'array di caratteri:

// 1. copy constructor
person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}

Nota la differenza tra inizializzazione e assegnazione: dobbiamo eliminare il vecchio stato prima di assegnarlo al name per evitare perdite di memoria. Inoltre, dobbiamo proteggere contro l'autoassegnazione del modulo x = x . Senza tale controllo, delete[] name cancellerebbe la matrice contenente la stringa sorgente , perché quando scrivi x = x , sia this->name che that.name contengono lo stesso puntatore.

Eccezione sicurezza

Sfortunatamente, questa soluzione fallirà se il new char[...] lancia un'eccezione a causa dell'esaurimento della memoria. Una possibile soluzione è introdurre una variabile locale e riordinare le affermazioni:

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

Questo si occupa anche dell'auto-assegnazione senza un controllo esplicito. Una soluzione ancora più robusta a questo problema è l' idioma della copia e dello swap , ma qui non entrerò nei dettagli della sicurezza delle eccezioni. Ho solo menzionato le eccezioni per fare il seguente punto: Scrivere classi che gestiscono risorse è difficile.

Risorse non copiabili

Alcune risorse non possono o non devono essere copiate, come handle di file o mutex. In tal caso, dichiara semplicemente il costruttore copia e l'operatore di assegnazione copia come private senza dare una definizione:

private:

    person(const person& that);
    person& operator=(const person& that);

In alternativa, puoi ereditare da boost::noncopyable o dichiararli come cancellati (C ++ 0x):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

La regola del tre

A volte è necessario implementare una classe che gestisce una risorsa. (Non gestisci mai più risorse in una singola classe, questo porterà solo a dolore.) In tal caso, ricorda la regola del tre :

Se è necessario dichiarare esplicitamente il distruttore, il costruttore della copia o l'operatore di assegnazione delle copie, è necessario dichiararli esplicitamente tutti e tre.

(Sfortunatamente, questa "regola" non è applicata dallo standard C ++ o da qualsiasi compilatore di cui sono a conoscenza.)

Consigli

Il più delle volte, non è necessario gestire una risorsa da soli, perché una classe esistente come std::string lo fa già per te. Basta confrontare il codice semplice usando un membro std::string per l'alternativa contorta e soggetta a errori usando un char* e dovresti essere convinto. Fintanto che rimani lontano dai membri puntatori grezzi, è improbabile che la regola dei tre riguardi il tuo codice.


Fondamentalmente se hai un distruttore (non il distruttore predefinito) significa che la classe che hai definito ha una certa allocazione di memoria. Supponiamo che la classe sia usata al di fuori da qualche codice cliente o da voi.

    MyClass x(a, b);
    MyClass y(c, d);
    x = y; // This is a shallow copy if assignment operator is not provided

Se MyClass ha solo alcuni membri tipizzati primitivi, un operatore di assegnazione predefinito funzionerebbe, ma se ha alcuni membri del puntatore e oggetti che non hanno operatori di assegnazione il risultato sarebbe imprevedibile. Quindi possiamo dire che se c'è qualcosa da eliminare nel distruttore di una classe, potremmo aver bisogno di un operatore di copia profonda, il che significa che dovremmo fornire un costruttore di copia e un operatore di assegnazione.


La Regola del Tre è una regola empirica per C ++, in pratica dicendo

Se la tua classe ha bisogno di qualcuno di

  • un costruttore di copie ,
  • un operatore di assegnazione ,
  • o un distruttore ,

definito esplicitamente, quindi è probabile che abbia bisogno di tutti e tre .

Le ragioni di ciò sono che tutti e tre sono solitamente utilizzati per gestire una risorsa e, se la classe gestisce una risorsa, in genere deve gestire la copia e la liberazione.

Se non esiste una buona semantica per copiare la risorsa gestita dalla classe, allora considera di proibire la copia dichiarando (non defining ) il costruttore di copia e l'operatore di assegnazione come private .

(Si noti che l'imminente nuova versione dello standard C ++ (che è C ++ 11) aggiunge la semantica del movimento al C ++, che probabilmente cambierà la Regola del 3. Tuttavia, ne so troppo poco per scrivere una sezione C ++ 11 sulla regola del tre).


La regola del terzo in C ++ è un principio fondamentale della progettazione e lo sviluppo di tre requisiti che, se esiste una chiara definizione in una delle seguenti funzioni membro, il programmatore dovrebbe definire insieme le altre due funzioni dei membri. Vale a dire le seguenti tre funzioni membro sono indispensabili: distruttore, costruttore di copia, operatore di assegnazione copia.

Copia costruttore in C ++ è un costruttore speciale. È usato per costruire un nuovo oggetto, che è il nuovo oggetto equivalente a una copia di un oggetto esistente.

L'operatore di assegnazione delle copie è un operatore di assegnazione speciale che viene solitamente utilizzato per specificare un oggetto esistente ad altri dello stesso tipo di oggetto.

Ci sono esempi veloci:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

Molte delle risposte esistenti toccano già il costruttore di copie, l'operatore di assegnazione e il distruttore. Tuttavia, nel post C ++ 11, l'introduzione della mossa semantica potrebbe espandere questo oltre 3.

Recentemente Michael Claisse ha tenuto un discorso che tocca questo argomento: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class





rule-of-three