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

30

Ci sono molti suggerimenti in C ++, ma per essere onesti tra 5 anni circa nella programmazione in 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 rispetto ai puntatori 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 puntatori è OK lasciarli "crudi" o dovrei usare qualcosa di più appropriato?

E nella seconda volta, se un datore di lavoro leggerà il codice, sarà severo su che tipo di indicatori che uso o no?

    
posta CheshireChild 28.02.2015 - 22:51
fonte

1 risposta

67

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

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... perderà memoria se una delete di accompagnamento non viene eseguita al momento opportuno.

auto_ptr

Per ridurre al minimo questi casi, è stato introdotto std::auto_ptr<> . A causa delle limitazioni del C ++ precedenti allo standard 2011, tuttavia, è ancora molto facile che auto_ptr elimini 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 casi d'uso più deboli è nei container. Questo perché se viene eseguita una copia di auto_ptr<> e la vecchia copia non viene reimpostata con attenzione, il contenitore può 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<> verrà ripulito correttamente, anche quando è passato tra le funzioni. Lo fa rappresentando semanticamente la "proprietà" del puntatore - il "proprietario" lo ripulisce. Questo lo rende ideale per l'uso in container:

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

Diversamente da auto_ptr<> , unique_ptr<> si comporta bene qui e quando vector ridimensiona, nessuno degli oggetti verrà eliminato per errore mentre vector copia il suo backing store.

shared_ptr e weak_ptr

unique_ptr<> è utile, per sicurezza, ma ci sono casi in cui vuoi che due parti della tua base di codice siano in grado di riferirsi allo stesso oggetto e copiare il puntatore, pur essendo comunque garantita la corretta pulizia. Ad esempio, un albero potrebbe apparire come 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à pulito in modo corretto quando tutte le copie del nodo radice verranno distrutte.

Funziona perché ogni shared_ptr<> trattiene non solo il puntatore all'oggetto, ma anche un conteggio di riferimento di tutti gli oggetti shared_ptr<> che si riferiscono allo stesso puntatore. Quando viene creato uno nuovo, il conteggio aumenta. Quando uno viene distrutto, il conteggio diminuisce. Quando il conteggio raggiunge lo zero, il puntatore è delete d.

Quindi questo introduce un problema: le strutture a collegamento doppio finiscono con riferimenti circolari. Supponiamo di voler aggiungere un puntatore parent 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 delete d perché il suo conteggio dei riferimenti non sarà mai pari a zero.

Per risolvere questo problema, usi 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 genitore. Rende l'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, dal momento che stai mantenendo un shared_ptr<> di esso.

make_shared e make_unique

Ora, ci sono alcuni problemi minori con shared_ptr<> e unique_ptr<> che dovrebbero essere indirizzati. 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 perdono memoria. Inoltre, shared_ptr<> mantiene il conteggio dei riferimenti lontano dall'oggetto a cui punta e questo può significa una seconda allocazione). Di solito non è consigliabile.

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 l'opportunità di creare il suo conteggio di riferimento nello stesso spazio di memoria come oggetto gestito, che può essere sia più veloce sia in grado di salvare pochi byte di memoria, mentre ti dà un 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 QObject s hanno un meccanismo in cui verranno distrutti correttamente senza la necessità di l'utente a delete di loro.

Non so come si comporterà QObject s quando gestito 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 io credo che Qt5 sia stato adattato per questo caso d'uso.

    
risposta data 28.02.2015 - 23:24
fonte

Leggi altre domande sui tag