raw, weak_ptr, unique_ptr, shared_ptr ecc ... Come sceglierli saggiamente?


33

Ci sono molti puntatori in C ++ ma ad essere sinceri tra circa 5 anni nella programmazione C ++ (in particolare con Qt Framework) uso solo il vecchio puntatore raw:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

So che ci sono molti altri puntatori "intelligenti":

// shared pointer:
shared_ptr<SomeKindofObject> Object;

// unique pointer:
unique_ptr<SomeKindofObject> Object;

// weak pointer:
weak_ptr<SomeKindofObject> Object;

Ma non ho la minima idea di cosa fare con loro e cosa possono offrirmi in confronto ai consigli grezzi.

Ad esempio ho questa intestazione di classe:

#ifndef LIBRARY
#define LIBRARY

class LIBRARY
{
public:
    // Permanent list that will be updated from time to time where
    // each items can be modified everywhere in the code:
    QList<ItemThatWillBeUsedEveryWhere*> listOfUselessThings; 
private:
    // Temporary reader that will read something to put in the list
    // and be quickly deleted:
    QSettings *_reader;
    // A dialog that will show something (just for the sake of example):
    QDialog *_dialog;
};

#endif 

Questo chiaramente non è esaustivo, ma per ognuno di questi 3 indicatori è OK lasciarli "grezzi" o dovrei usare qualcosa di più appropriato?

E la seconda volta, se un datore di lavoro leggerà il codice, sarà rigoroso sul tipo di puntatori che utilizzo o no?


Questo argomento sembra così appropriato per SO. questo era nel 2008 . Ed ecco che tipo di puntatore devo usare quando? . Sono sicuro che puoi trovare abbinamenti ancora migliori. Questi sono stati solo il primo che ho visto
sehe

imo questo limite poiché si tratta tanto del significato / intento concettuale di queste classi quanto dei dettagli tecnici del loro comportamento e delle loro implementazioni. Dato che la risposta accettata si sporge verso la prima, sono contento che questa sia la "versione PSE" di quella domanda SO.
Ixrec

Risposte:


70

Un puntatore "grezzo" non è gestito. Cioè, la seguente riga:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... perderà memoria se un accompagnamento deletenon viene eseguito al momento opportuno.

auto_ptr

Al fine di minimizzare questi casi, è std::auto_ptr<>stato introdotto. A causa delle limitazioni di C ++ precedenti allo standard del 2011, tuttavia, è ancora molto facile auto_ptrperdere la memoria. È sufficiente per casi limitati, come questo, tuttavia:

void func() {
    std::auto_ptr<SomeKindOfObject> sKOO_ptr(new SomeKindOfObject());
    // do some work
    // will not leak if you do not copy sKOO_ptr.
}

Uno dei suoi casi d'uso più deboli è nei container. Questo perché se auto_ptr<>viene creata una copia di una copia precedente e la copia precedente non viene accuratamente ripristinata, il contenitore potrebbe eliminare il puntatore e perdere i dati.

unique_ptr

In sostituzione, C ++ 11 ha introdotto std::unique_ptr<>:

void func2() {
    std::unique_ptr<SomeKindofObject> sKOO_unique(new SomeKindOfObject());

    func3(sKOO_unique); // now func3() owns the pointer and sKOO_unique is no longer valid
}

Tale unique_ptr<>sarà ripulito correttamente, anche quando viene passato tra le funzioni. Lo fa rappresentando semanticamente la "proprietà" del puntatore: il "proprietario" lo pulisce. Questo lo rende ideale per l'uso in contenitori:

std::vector<std::unique_ptr<SomeKindofObject>> sKOO_vector();

Diversamente auto_ptr<>, qui unique_ptr<>è ben educato e quando i vectorridimensionamenti, nessuno degli oggetti verrà accidentalmente cancellato mentre le vectorcopie sono archiviate.

shared_ptr e weak_ptr

unique_ptr<>è utile, certo, ma ci sono casi in cui si desidera che due parti della base di codice siano in grado di fare riferimento allo stesso oggetto e copiare il puntatore in giro, pur garantendo comunque una corretta pulizia. Ad esempio, un albero potrebbe assomigliare a questo, quando si utilizza std::shared_ptr<>:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

In questo caso, possiamo persino conservare più copie di un nodo radice e l'albero verrà ripulito correttamente quando tutte le copie del nodo radice vengono distrutte.

Questo funziona perché ognuno di essi shared_ptr<>tiene conto non solo del puntatore all'oggetto, ma anche di un conteggio di riferimento di tutti gli shared_ptr<>oggetti che si riferiscono allo stesso puntatore. Quando ne viene creato uno nuovo, il conteggio aumenta. Quando uno viene distrutto, il conteggio diminuisce. Quando il conteggio raggiunge lo zero, il puntatore è deleted.

Quindi questo introduce un problema: le strutture a doppio collegamento finiscono con riferimenti circolari. Supponiamo di voler aggiungere un parentpuntatore al nostro albero Node:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Ora, se rimuoviamo un Node, c'è un riferimento ciclico ad esso. Non sarà mai deleted perché il suo conteggio di riferimento non sarà mai zero.

Per risolvere questo problema, si utilizza un std::weak_ptr<>:

template<class T>
struct Node {
    T value;
    std::weak_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Ora, le cose funzioneranno correttamente e la rimozione di un nodo non lascerà riferimenti bloccati al nodo padre. Rende il camminare sull'albero un po 'più complicato, tuttavia:

std::shared_ptr<Node<T>> parent_of_this = node->parent.lock();

In questo modo, puoi bloccare un riferimento al nodo e hai una ragionevole garanzia che non scomparirà mentre ci stai lavorando, dato che ti stai aggrappando a uno shared_ptr<>di essi.

make_shared e make_unique

Ora, ci sono alcuni problemi minori con shared_ptr<>e unique_ptr<>che dovrebbero essere affrontati. Le seguenti due righe hanno un problema:

foo_unique(std::unique_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());
foo_shared(std::shared_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());

Se thrower()genera un'eccezione, entrambe le linee perderanno memoria. E più di questo, shared_ptr<>tiene il conteggio dei riferimenti lontano dall'oggetto a cui punta e questo può significare una seconda allocazione). Di solito non è desiderabile.

C ++ 11 fornisce std::make_shared<>()e C ++ 14 fornisce std::make_unique<>()per risolvere questo problema:

foo_unique(std::make_unique<SomeKindofObject>(), thrower());
foo_shared(std::make_shared<SomeKindofObject>(), thrower());

Ora, in entrambi i casi, anche se thrower()genera un'eccezione, non ci sarà una perdita di memoria. Come bonus, make_shared<>()ha la possibilità di creare il suo conteggio di riferimento nello stesso spazio di memoria dell'oggetto gestito, che può essere sia più veloce che in grado di risparmiare qualche byte di memoria, offrendo una garanzia di sicurezza eccezionale!

Note su Qt

Va notato, tuttavia, che Qt, che deve supportare i compilatori pre-C ++ 11, ha il proprio modello di garbage collection: molti QObjecthanno un meccanismo in cui verranno distrutti correttamente senza che l'utente debbadelete .

Non so come QObjectsi comporteranno se gestiti da puntatori gestiti C ++ 11, quindi non posso dire che shared_ptr<QDialog>sia una buona idea. Non ho abbastanza esperienza con Qt per dirlo con certezza, ma credo che Qt5 sia stato adattato per questo caso d'uso.


1
@Zilators: si prega di notare il mio commento aggiunto su Qt. La risposta alla tua domanda sull'opportunità di gestire tutti e tre i puntatori dipende dal comportamento corretto degli oggetti Qt.
Greyfade,

2
"entrambi fanno allocazione separata per contenere il puntatore"? No, unique_ptr non alloca mai nulla in più, solo shared_ptr deve allocare un numero di riferimento + allocatore-oggetto. "entrambe le linee perderanno memoria"? no, solo potrebbe, nemmeno una garanzia di cattivo comportamento.
Deduplicatore,

1
@Deduplicator: la mia formulazione deve essere stata poco chiara: si shared_ptrtratta di un oggetto separato - un'allocazione separata - dall'oggetto newed. Esistono in luoghi diversi. make_sharedha la capacità di metterli insieme nella stessa posizione, il che migliora la localizzazione della cache, tra le altre cose.
Greyfade,

2
@greyfade: Nononono. shared_ptrè un oggetto. E per gestire un oggetto, deve allocare un oggetto (conteggi di riferimento (debole + forte) + distruttore). make_sharedconsente di allocare quello e l'oggetto gestito come un unico pezzo. unique_ptrnon li utilizza, quindi non vi è alcun vantaggio corrispondente, oltre a garantire che l'oggetto sia sempre di proprietà dello smart-pointer. Per inciso, si può avere un shared_ptrche possiede un oggetto sottostante e rappresenta un nullptr, o che non possiede e rappresenta un puntatore non null.
Deduplicatore,

1
L'ho guardato e sembra esserci una confusione generale su ciò che shared_ptrfa: 1. Condivide la proprietà di un oggetto (rappresentato da un oggetto allocato dinamicamente interno con un conteggio di riferimento debole e forte, nonché un deleter) . 2. Contiene un puntatore. Queste due parti sono indipendenti. make_uniqueed make_sharedentrambi assicurano che l'oggetto allocato sia messo in modo sicuro in un puntatore intelligente. Inoltre, make_sharedconsente di allocare l'oggetto proprietà e il puntatore gestito insieme.
Deduplicatore,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.