C++
Funzioni speciali per gli utenti
Ricerca…
Distruttori virtuali e protetti
Una classe progettata per essere ereditata da una classe base viene chiamata. Bisogna fare attenzione con le funzioni speciali dei membri di tale classe.
Una classe progettata per essere utilizzata polimorficamente in fase di esecuzione (tramite un puntatore alla classe base) dovrebbe dichiarare il distruttore virtual
. Ciò consente di distruggere correttamente le parti derivate dell'oggetto, anche quando l'oggetto viene distrutto attraverso un puntatore alla classe base.
class Base {
public:
virtual ~Base() = default;
private:
// data members etc.
};
class Derived : public Base { // models Is-A relationship
public:
// some methods
private:
// more data members
};
// virtual destructor in Base ensures that derived destructors
// are also called when the object is destroyed
std::unique_ptr<Base> base = std::make_unique<Derived>();
base = nullptr; // safe, doesn't leak Derived's members
Se la classe non ha bisogno di essere polimorfica, ma deve comunque consentire che la sua interfaccia sia ereditata, utilizzare un distruttore non virtuale protected
.
class NonPolymorphicBase {
public:
// some methods
protected:
~NonPolymorphicBase() = default; // note: non-virtual
private:
// etc.
};
Una tale classe non può mai essere distrutta attraverso un puntatore, evitando perdite silenziose dovute alla divisione.
Questa tecnica si applica in particolare alle classi progettate per essere classi base private
. Tale classe potrebbe essere utilizzata per incapsulare alcuni dettagli di implementazione comuni, fornendo al tempo stesso metodi virtual
come punti di personalizzazione. Questo tipo di classe non dovrebbe mai essere utilizzato in modo polimorfico e un distruttore protected
aiuta a documentare questo requisito direttamente nel codice.
Infine, alcune classi potrebbero richiedere che non vengano mai utilizzate come classe base. In questo caso, la classe può essere contrassegnata come final
. In questo caso, un normale distruttore pubblico non virtuale va bene.
class FinalClass final { // marked final here
public:
~FinalClass() = default;
private:
// etc.
};
Sposta e copia impliciti
Ricordare che la dichiarazione di un distruttore impedisce al compilatore di generare costruttori di movimento impliciti e spostare gli operatori di assegnazione. Se dichiari un distruttore, ricorda di aggiungere anche le definizioni appropriate per le operazioni di spostamento.
Inoltre, la dichiarazione delle operazioni di spostamento sopprimerà la generazione delle operazioni di copia, quindi anche queste dovrebbero essere aggiunte (se agli oggetti di questa classe è richiesto di avere semantica della copia).
class Movable {
public:
virtual ~Movable() noexcept = default;
// compiler won't generate these unless we tell it to
// because we declared a destructor
Movable(Movable&&) noexcept = default;
Movable& operator=(Movable&&) noexcept = default;
// declaring move operations will suppress generation
// of copy operations unless we explicitly re-enable them
Movable(const Movable&) = default;
Movable& operator=(const Movable&) = default;
};
Copia e scambia
Se stai scrivendo una classe che gestisce le risorse, devi implementare tutte le funzioni dei membri speciali (vedi Regola di tre / cinque / zero ). L'approccio più diretto alla scrittura del costruttore di copie e dell'operatore di assegnazione sarebbe:
person(const person &other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
person& operator=(person const& rhs) {
if (this != &other) {
delete [] name;
name = new char[std::strlen(other.name) + 1];
std::strcpy(name, other.name);
age = other.age;
}
return *this;
}
Ma questo approccio ha alcuni problemi. Fallisce la forte garanzia di eccezione - se i new[]
lanci new[]
, abbiamo già eliminato le risorse possedute da this
e non possiamo recuperare. Stiamo duplicando gran parte della logica della costruzione delle copie nell'assegnazione delle copie. E dobbiamo ricordare il controllo dell'auto-assegnazione, che di solito aggiunge solo un sovraccarico all'operazione di copia, ma è ancora critico.
Per soddisfare la forte garanzia di eccezione ed evitare la duplicazione del codice (doppio quindi con il successivo operatore di assegnazione del movimento), possiamo usare l'idioma copy-and-swap:
class person {
char* name;
int age;
public:
/* all the other functions ... */
friend void swap(person& lhs, person& rhs) {
using std::swap; // enable ADL
swap(lhs.name, rhs.name);
swap(lhs.age, rhs.age);
}
person& operator=(person rhs) {
swap(*this, rhs);
return *this;
}
};
Perché funziona? Considera cosa succede quando abbiamo
person p1 = ...;
person p2 = ...;
p1 = p2;
Per prima cosa, copia-costruisci rhs
da p2
(che non abbiamo dovuto duplicare qui). Se l'operazione viene lanciata, non facciamo nulla in operator=
e p1
rimane intatto. Successivamente, scambiamo i membri tra *this
e rhs
, e quindi rhs
esce dall'ambito. Quando operator=
, pulisce implicitamente le risorse originali di this
(tramite il distruttore, che non abbiamo dovuto duplicare). Anche l'autoassegnazione funziona - è meno efficiente con copy-and-swap (comporta un'allocazione e una deallocazione aggiuntive), ma se questo è lo scenario improbabile, non rallentiamo il tipico caso d'uso per renderlo conto.
La formulazione sopra funziona come-è già per l'assegnazione del movimento.
p1 = std::move(p2);
Qui, spostiamo-costruisci rhs
da p2
, e tutto il resto è altrettanto valido. Se una classe è mobile ma non è copiabile, non è necessario cancellare l'assegnazione della copia, poiché questo operatore di assegnazione sarà semplicemente mal formato a causa del costruttore di copie cancellato.
Costruttore predefinito
Un costruttore predefinito è un tipo di costruttore che non richiede parametri quando viene chiamato. Prende il nome dal tipo che costruisce ed è una funzione membro di esso (come lo sono tutti i costruttori).
class C{
int i;
public:
// the default constructor definition
C()
: i(0){ // member initializer list -- initialize i to 0
// constructor function body -- can do more complex things here
}
};
C c1; // calls default constructor of C to create object c1
C c2 = C(); // calls default constructor explicitly
C c3(); // ERROR: this intuitive version is not possible due to "most vexing parse"
C c4{}; // but in C++11 {} CAN be used in a similar way
C c5[2]; // calls default constructor for both array elements
C* c6 = new C[2]; // calls default constructor for both array elements
Un altro modo per soddisfare il requisito "nessun parametro" è che lo sviluppatore fornisca valori predefiniti per tutti i parametri:
class D{
int i;
int j;
public:
// also a default constructor (can be called with no parameters)
D( int i = 0, int j = 42 )
: i(i), j(j){
}
};
D d; // calls constructor of D with the provided default values for the parameters
In alcune circostanze (cioè, lo sviluppatore non fornisce costruttori e non ci sono altre condizioni di squalifica), il compilatore fornisce implicitamente un costruttore predefinito vuoto:
class C{
std::string s; // note: members need to be default constructible themselves
};
C c1; // will succeed -- C has an implicitly defined default constructor
Avere qualche altro tipo di costruttore è una delle condizioni di squalifica menzionate in precedenza:
class C{
int i;
public:
C( int i ) : i(i){}
};
C c1; // Compile ERROR: C has no (implicitly defined) default constructor
Per evitare la creazione implicita di un costruttore predefinito, una tecnica comune è dichiararla come private
(senza definizione). L'intenzione è quella di causare un errore di compilazione quando qualcuno tenta di utilizzare il costruttore (questo risulta in un errore di Access to private o di linker, a seconda del compilatore).
Per essere sicuri che un costruttore predefinito (funzionalmente simile a quello implicito) sia definito, uno sviluppatore potrebbe scriverne uno vuoto in modo esplicito.
In C ++ 11, uno sviluppatore può anche usare la parola chiave delete
per impedire al compilatore di fornire un costruttore predefinito.
class C{
int i;
public:
// default constructor is explicitly deleted
C() = delete;
};
C c1; // Compile ERROR: C has its default constructor deleted
Inoltre, uno sviluppatore potrebbe anche essere esplicito sul volere che il compilatore fornisca un costruttore predefinito.
class C{
int i;
public:
// does have automatically generated default constructor (same as implicit one)
C() = default;
C( int i ) : i(i){}
};
C c1; // default constructed
C c2( 1 ); // constructed with the int taking constructor
È possibile determinare se un tipo ha un costruttore predefinito (o è un tipo primitivo) utilizzando std::is_default_constructible
da <type_traits>
:
class C1{ };
class C2{ public: C2(){} };
class C3{ public: C3(int){} };
using std::cout; using std::boolalpha; using std::endl;
using std::is_default_constructible;
cout << boolalpha << is_default_constructible<int>() << endl; // prints true
cout << boolalpha << is_default_constructible<C1>() << endl; // prints true
cout << boolalpha << is_default_constructible<C2>() << endl; // prints true
cout << boolalpha << is_default_constructible<C3>() << endl; // prints false
In C ++ 11, è ancora possibile utilizzare la versione non-functor di std::is_default_constructible
:
cout << boolalpha << is_default_constructible<C1>::value << endl; // prints true
Distruttore
Un distruttore è una funzione senza argomenti chiamata quando un oggetto definito dall'utente sta per essere distrutto. Prende il nome dal tipo che distrugge con un prefisso ~
.
class C{
int* is;
string s;
public:
C()
: is( new int[10] ){
}
~C(){ // destructor definition
delete[] is;
}
};
class C_child : public C{
string s_ch;
public:
C_child(){}
~C_child(){} // child destructor
};
void f(){
C c1; // calls default constructor
C c2[2]; // calls default constructor for both elements
C* c3 = new C[2]; // calls default constructor for both array elements
C_child c_ch; // when destructed calls destructor of s_ch and of C base (and in turn s)
delete[] c3; // calls destructors on c3[0] and c3[1]
} // automatic variables are destroyed here -- i.e. c1, c2 and c_ch
Nella maggior parte dei casi (cioè, un utente non fornisce alcun distruttore e non ci sono altre condizioni di squalifica), il compilatore fornisce implicitamente un distruttore predefinito:
class C{
int i;
string s;
};
void f(){
C* c1 = new C;
delete c1; // C has a destructor
}
class C{
int m;
private:
~C(){} // not public destructor!
};
class C_container{
C c;
};
void f(){
C_container* c_cont = new C_container;
delete c_cont; // Compile ERROR: C has no accessible destructor
}
In C ++ 11, uno sviluppatore può sovrascrivere questo comportamento impedendo al compilatore di fornire un distruttore predefinito.
class C{
int m;
public:
~C() = delete; // does NOT have implicit destructor
};
void f{
C c1;
} // Compile ERROR: C has no destructor
Inoltre, uno sviluppatore potrebbe anche essere esplicito sul fatto che il compilatore debba fornire un distruttore predefinito.
class C{
int m;
public:
~C() = default; // saying explicitly it does have implicit/empty destructor
};
void f(){
C c1;
} // C has a destructor -- c1 properly destroyed
È possibile determinare se un tipo ha un distruttore (o è un tipo primitivo) utilizzando std::is_destructible
da <type_traits>
:
class C1{ };
class C2{ public: ~C2() = delete };
class C3 : public C2{ };
using std::cout; using std::boolalpha; using std::endl;
using std::is_destructible;
cout << boolalpha << is_destructible<int>() << endl; // prints true
cout << boolalpha << is_destructible<C1>() << endl; // prints true
cout << boolalpha << is_destructible<C2>() << endl; // prints false
cout << boolalpha << is_destructible<C3>() << endl; // prints false