C++
La regola del tre, cinque e zero
Ricerca…
Regola del Cinque
C ++ 11 introduce due nuove funzioni membro speciali: il costruttore di movimento e l'operatore di assegnazione del movimento. Per tutti gli stessi motivi per cui vuoi seguire la Regola dei Tre in C ++ 03, di solito vuoi seguire la Regola dei Cinque in C ++ 11: Se una classe richiede UNA delle cinque funzioni membro speciali, e se muove la semantica sono desiderati, quindi molto probabilmente richiede TUTTI i CINQUE di loro.
Nota, tuttavia, che non seguire la Regola dei Cinque non viene considerato un errore, ma un'opportunità di ottimizzazione persa, purché la Regola del Tre sia ancora seguita. Se nessun costruttore di movimento o operatore di assegnazione movimento è disponibile quando il compilatore normalmente ne usa uno, utilizzerà semantica di copia, se possibile, risultando in un'operazione meno efficiente a causa di operazioni di copia non necessarie. Se non si desidera spostare la semantica per una classe, non è necessario dichiarare un costruttore di movimenti o un operatore di assegnazione.
Lo stesso esempio della regola del tre:
class Person
{
char* name;
int age;
public:
// Destructor
~Person() { delete [] name; }
// Implement Copy Semantics
Person(Person const& other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
Person &operator=(Person const& other)
{
// Use copy and swap idiom to implement assignment.
Person copy(other);
swap(*this, copy);
return *this;
}
// Implement Move Semantics
// Note: It is usually best to mark move operators as noexcept
// This allows certain optimizations in the standard library
// when the class is used in a container.
Person(Person&& that) noexcept
: name(nullptr) // Set the state so we know it is undefined
, age(0)
{
swap(*this, that);
}
Person& operator=(Person&& that) noexcept
{
swap(*this, that);
return *this;
}
friend void swap(Person& lhs, Person& rhs) noexcept
{
std::swap(lhs.name, rhs.name);
std::swap(lhs.age, rhs.age);
}
};
In alternativa, sia l'operatore di assegnazione di copia che di spostamento può essere sostituito con un singolo operatore di assegnazione, che prende un'istanza in base al valore anziché al riferimento o al riferimento di rvalue per facilitare l'uso dell'idioma copy-and-swap.
Person& operator=(Person copy)
{
swap(*this, copy);
return *this;
}
Estendere dalla Regola dei Tre alla Regola dei Cinque è importante per i motivi di prestazione, ma nella maggior parte dei casi non è strettamente necessario. L'aggiunta del costruttore di copia e dell'operatore di assegnazione garantisce che lo spostamento del tipo non crei memoria (la costruzione delle mosse ricadrà semplicemente sulla copia in quel caso), ma eseguirà copie che il chiamante probabilmente non ha previsto.
Regola dello zero
Possiamo combinare i principi della Rule of Five e RAII per ottenere un'interfaccia molto più snella: la regola dello zero: qualsiasi risorsa che deve essere gestita dovrebbe essere del suo tipo. Quel tipo dovrebbe seguire la Regola del Cinque, ma tutti gli utenti di quella risorsa non hanno bisogno di scrivere nessuna delle cinque funzioni membro speciali e possono semplicemente default
tutte.
Utilizzando la classe Person
introdotta nell'esempio della regola del tre , possiamo creare un oggetto di gestione delle risorse per le cstrings
:
class cstring {
private:
char* p;
public:
~cstring() { delete [] p; }
cstring(cstring const& );
cstring(cstring&& );
cstring& operator=(cstring const& );
cstring& operator=(cstring&& );
/* other members as appropriate */
};
E una volta che questo è separato, la nostra classe Person
diventa molto più semplice:
class Person {
cstring name;
int arg;
public:
~Person() = default;
Person(Person const& ) = default;
Person(Person&& ) = default;
Person& operator=(Person const& ) = default;
Person& operator=(Person&& ) = default;
/* other members as appropriate */
};
I membri speciali in Person
non hanno nemmeno bisogno di essere dichiarati esplicitamente; il compilatore verrà predefinito o cancellato in modo appropriato, in base al contenuto di Person
. Pertanto, il seguente è anche un esempio della regola zero.
struct Person {
cstring name;
int arg;
};
Se la cstring
dovesse essere un tipo di solo spostamento, con un operatore di costruzione / assegnazione della copia di delete
d, allora anche Person
sposterebbe automaticamente.
Il termine regola zero è stato introdotto da R. Martinho Fernandes
Regola del tre
La Regola dei tre stabilisce che se un tipo ha bisogno di avere un costruttore di copia definito dall'utente, un operatore di assegnazione copia o un distruttore, allora deve avere tutti e tre .
Il motivo della regola è che una classe che ha bisogno di uno dei tre gestisce alcune risorse (handle di file, memoria allocata dinamicamente, ecc.) E tutte e tre sono necessarie per gestire coerentemente tale risorsa. Le funzioni di copia riguardano il modo in cui la risorsa viene copiata tra gli oggetti e il distruttore distrugge la risorsa, in accordo con i principi RAII .
Considera un tipo che gestisce una risorsa stringa:
class Person
{
char* name;
int age;
public:
Person(char const* new_name, int new_age)
: name(new char[std::strlen(new_name) + 1])
, age(new_age)
{
std::strcpy(name, new_name);
}
~Person() {
delete [] name;
}
};
Poiché il name
stato assegnato nel costruttore, il distruttore lo rilascia per evitare perdite di memoria. Ma cosa succede se un tale oggetto viene copiato?
int main()
{
Person p1("foo", 11);
Person p2 = p1;
}
Innanzitutto, p1
sarà costruito. Quindi p2
verrà copiato da p1
. Tuttavia, il costruttore di copie generato da C ++ copierà ogni componente del tipo così com'è. Il che significa che p1.name
e p2.name
puntano entrambi alla stessa stringa.
Quando main
estremità, saranno chiamati distruttori. Il primo distruttore di p2
sarà chiamato; cancellerà la stringa. Quindi verrà chiamato il distruttore di p1
. Tuttavia, la stringa è già stata eliminata . Chiamare la delete
sulla memoria che era già stata eliminata produce un comportamento indefinito.
Per evitare ciò, è necessario fornire un costruttore di copia adatto. Un approccio consiste nell'implementare un sistema di conteggio di riferimento, in cui diverse istanze di Person
condividono gli stessi dati di stringa. Ogni volta che viene eseguita una copia, il conteggio dei riferimenti condivisi viene incrementato. Il distruttore decrementa quindi il conteggio dei riferimenti, rilasciando la memoria solo se il conteggio è zero.
Oppure potremmo implementare la semantica del valore e il comportamento di copia profonda :
Person(Person const& other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
Person &operator=(Person const& other)
{
// Use copy and swap idiom to implement assignment
Person copy(other);
swap(copy); // assume swap() exchanges contents of *this and copy
return *this;
}
L'implementazione dell'operatore di assegnazione delle copie è complicata dalla necessità di rilasciare un buffer esistente. La tecnica di copia e swap crea un oggetto temporaneo che contiene un nuovo buffer. Scambiare il contenuto di *this
e copy
dà la proprietà alla copy
del buffer originale. La distruzione della copy
, al ripristino della funzione, rilascia il buffer precedentemente di proprietà di *this
.
Protezione di autoassegnazione
Quando si scrive un operatore di assegnazione copia, è molto importante che sia in grado di lavorare in caso di autoassegnazione. Cioè, deve permettere questo:
SomeType t = ...;
t = t;
L'autoassegnazione di solito non avviene in modo così ovvio. In genere avviene tramite un percorso tortuoso attraverso vari sistemi di codice, in cui la posizione dell'assegnazione ha semplicemente due puntatori o riferimenti Person
e non ha idea di essere lo stesso oggetto.
Qualsiasi operatore di assegnazione copia che scrivi deve essere in grado di tenerne conto.
Il modo tipico per farlo è quello di avvolgere tutta la logica di assegnazione in una condizione come questa:
SomeType &operator=(const SomeType &other)
{
if(this != &other)
{
//Do assignment logic.
}
return *this;
}
Nota: è importante pensare all'assegnazione di sé e assicurarsi che il codice si comporti correttamente quando ciò accade. Tuttavia, l'autoassegnazione è un evento molto raro e l'ottimizzazione per evitare che possa effettivamente pessimizzare il caso normale. Dal momento che il caso normale è molto più comune, pessimizzare l'autoassegnazione può ridurre l'efficienza del codice (quindi fai attenzione ad usarlo).
Ad esempio, la normale tecnica per implementare l'operatore di assegnazione è l' copy and swap idiom
. La normale implementazione di questa tecnica non si preoccupa di testare l'autoassegnazione (anche se l'autoassegnazione è costosa perché viene fatta una copia). La ragione è che la pessimizzazione del caso normale ha dimostrato di essere molto più costosa (come accade più spesso).
Spostare gli operatori di assegnazione deve anche essere protetto contro l'autoassegnazione. Tuttavia, la logica di molti di questi operatori è basata su std::swap
, che può gestire lo scambio da / verso la stessa memoria. Quindi, se la tua logica di assegnazione del movimento non è altro che una serie di operazioni di scambio, allora non hai bisogno di una protezione per l'assegnazione automatica.
Se questo non è il tuo caso, devi prendere misure simili come sopra.