Adottato da qui .
La maggior parte dei modelli nella libreria standard C ++ richiede che vengano istanziati con tipi completi. Tuttavia shared_ptr
e unique_ptr
sono eccezioni parziali . Alcuni, ma non tutti i loro membri, possono essere istanziati con tipi incompleti. La motivazione di ciò è supportare idiomi come il pimpl usando i puntatori intelligenti e senza rischiare comportamenti indefiniti.
Un comportamento indefinito può verificarsi quando si dispone di un tipo incompleto e lo si chiama delete
:
class A;
A* a = ...;
delete a;
Quanto sopra è un codice legale. Si compilerà. Il compilatore può o meno emettere un avviso per il codice sopra come quello sopra. Quando viene eseguito, probabilmente accadranno cose brutte. Se sei molto fortunato, il tuo programma andrà in crash. Tuttavia, un risultato più probabile è che il tuo programma perderà silenziosamente memoria come ~A()
non verrà chiamato.
L'uso auto_ptr<A>
nell'esempio sopra non aiuta. Ottieni ancora lo stesso comportamento indefinito come se avessi usato un puntatore non elaborato.
Tuttavia, l'uso di lezioni incomplete in determinati luoghi è molto utile! Questo è dove shared_ptr
e unique_ptr
aiuto. L'uso di uno di questi suggerimenti intelligenti ti permetterà di cavartela con un tipo incompleto, tranne dove è necessario avere un tipo completo. E, soprattutto, quando è necessario disporre di un tipo completo, si ottiene un errore di compilazione se si tenta di utilizzare il puntatore intelligente con un tipo incompleto in quel punto.
Niente più comportamenti indefiniti:
Se il tuo codice viene compilato, hai utilizzato un tipo completo ovunque sia necessario.
class A
{
class impl;
std::unique_ptr<impl> ptr_; // ok!
public:
A();
~A();
// ...
};
shared_ptr
e unique_ptr
richiede un tipo completo in luoghi diversi. Le ragioni sono oscure, hanno a che fare con un deleter dinamico rispetto a un deleter statico. Le ragioni precise non sono importanti. In effetti, nella maggior parte dei codici non è molto importante sapere esattamente dove è richiesto un tipo completo. Basta codificare e, se sbagli, il compilatore te lo dirà.
Tuttavia, nel caso in cui ti sia utile, ecco una tabella che documenta diversi membri shared_ptr
e unique_ptr
rispetto ai requisiti di completezza. Se il membro richiede un tipo completo, la voce ha una "C", altrimenti la voce della tabella viene riempita con "I".
Complete type requirements for unique_ptr and shared_ptr
unique_ptr shared_ptr
+------------------------+---------------+---------------+
| P() | I | I |
| default constructor | | |
+------------------------+---------------+---------------+
| P(const P&) | N/A | I |
| copy constructor | | |
+------------------------+---------------+---------------+
| P(P&&) | I | I |
| move constructor | | |
+------------------------+---------------+---------------+
| ~P() | C | I |
| destructor | | |
+------------------------+---------------+---------------+
| P(A*) | I | C |
+------------------------+---------------+---------------+
| operator=(const P&) | N/A | I |
| copy assignment | | |
+------------------------+---------------+---------------+
| operator=(P&&) | C | I |
| move assignment | | |
+------------------------+---------------+---------------+
| reset() | C | I |
+------------------------+---------------+---------------+
| reset(A*) | C | C |
+------------------------+---------------+---------------+
Qualsiasi operazione che richiede conversioni di puntatori richiede tipi completi per entrambi unique_ptr
e shared_ptr
.
Il unique_ptr<A>{A*}
costruttore può cavarsela con un incompleto A
solo se al compilatore non è richiesto di impostare una chiamata ~unique_ptr<A>()
. Ad esempio se metti l' unique_ptr
heap, puoi farcela con un incompleto A
. Maggiori dettagli su questo punto sono disponibili nella risposta di BarryTheHatchet qui .
shared_ptr
/unique_ptr
" di Howard Hinnant La tabella alla fine dovrebbe rispondere alla tua domanda.