Utilizzo di puntatori intelligenti per i membri della classe


159

Sto riscontrando problemi nel comprendere l'utilizzo dei puntatori intelligenti come membri della classe in C ++ 11. Ho letto molto sui puntatori intelligenti e penso di capire come unique_ptre shared_ptr/ o weak_ptrlavorare in generale. Quello che non capisco è il reale utilizzo. Sembra che tutti raccomandino di usarlo unique_ptrcome modo per andare quasi sempre. Ma come implementerei qualcosa del genere:

class Device {
};

class Settings {
    Device *device;
public:
    Settings(Device *device) {
        this->device = device;
    }

    Device *getDevice() {
        return device;
    }
};    

int main() {
    Device *device = new Device();
    Settings settings(device);
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

Diciamo che vorrei sostituire i puntatori con puntatori intelligenti. A unique_ptrnon funzionerebbe a causa di getDevice(), giusto? Quindi è il momento in cui uso shared_ptre weak_ptr? Nessun modo di usare unique_ptr? Mi sembra che per la maggior parte dei casi shared_ptrabbia più senso se non sto usando un puntatore in un ambito davvero piccolo?

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> device) {
        this->device = device;
    }

    std::weak_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::weak_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

È questa la strada da percorrere? Grazie mille!


4
Aiuta ad essere veramente chiaro su vita, proprietà e possibili null. Ad esempio, dopo essere passato deviceal costruttore di settings, vuoi essere in grado di fare riferimento ad esso nell'ambito di chiamata o solo tramite settings? Se quest'ultimo, unique_ptrè utile. Inoltre, hai uno scenario in cui il valore restituito getDevice()è null. In caso contrario, restituisci solo un riferimento.
Keith,

2
Sì, a shared_ptrè corretto in 8/10 casi. Gli altri 2/10 sono divisi tra unique_ptre weak_ptr. Inoltre, weak_ptrviene generalmente utilizzato per rompere i riferimenti circolari; Non sono sicuro che il tuo utilizzo sia considerato corretto.
Collin Dauphinee,

2
Prima di tutto, quale proprietà desideri per il devicemembro dei dati? Devi prima decidere quello.
juanchopanza,

1
Ok, capisco che come chiamante potrei usare un unique_ptrinvece e rinunciare alla proprietà quando chiamo il costruttore, se so che per ora non ne avrò più bisogno. Ma come progettista della Settingsclasse non so se anche il chiamante vuole mantenere un riferimento. Forse il dispositivo verrà utilizzato in molti luoghi. Ok, forse è esattamente questo il punto. In tal caso, non sarei l'unico proprietario ed è allora che userò shared_ptr, immagino. E: quindi i punti smart sostituiscono i puntatori, ma non i riferimenti, giusto?
michaelk,

this-> device = device; Utilizzare anche elenchi di inizializzazione.
Nils,

Risposte:


202

A unique_ptrnon funzionerebbe a causa di getDevice(), giusto?

No, non necessariamente. Ciò che è importante qui è determinare la politica di proprietà appropriata per il tuo Deviceoggetto, ovvero chi sarà il proprietario dell'oggetto a cui punta il tuo puntatore (intelligente).

E 'intenzione di essere l'istanza Settingsdell'oggetto da solo ? L' Deviceoggetto dovrà essere distrutto automaticamente quando l' Settingsoggetto viene distrutto o dovrebbe sopravvivere a quell'oggetto?

Nel primo caso, std::unique_ptrè ciò di cui hai bisogno, poiché rende Settingsl'unico (unico) proprietario dell'oggetto appuntito e l'unico oggetto responsabile della sua distruzione.

In base a questo presupposto, getDevice()dovrebbe restituire un semplice puntatore osservante (i puntatori osservanti sono puntatori che non mantengono in vita l'oggetto appuntito). Il tipo più semplice di osservazione puntatore è un puntatore non elaborato:

#include <memory>

class Device {
};

class Settings {
    std::unique_ptr<Device> device;
public:
    Settings(std::unique_ptr<Device> d) {
        device = std::move(d);
    }

    Device* getDevice() {
        return device.get();
    }
};

int main() {
    std::unique_ptr<Device> device(new Device());
    Settings settings(std::move(device));
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

[ NOTA 1: ti starai chiedendo perché sto usando puntatori non elaborati qui, quando tutti continuano a dire che i puntatori non elaborati sono dannosi, non sicuri e pericolosi. In realtà, questo è un avvertimento prezioso, ma è importante metterlo nel contesto corretto: i puntatori non elaborati sono dannosi quando vengono utilizzati per eseguire la gestione manuale della memoria , vale a dire l'allocazione e la deallocazione di oggetti attraverso newe delete. Se usato esclusivamente come mezzo per raggiungere la semantica di riferimento e passare attorno al non-possesso, osservando i puntatori, non c'è nulla di intrinsecamente pericoloso nei puntatori grezzi, tranne forse per il fatto che si dovrebbe fare attenzione a non dedurre un puntatore penzolante. - FINE NOTA 1 ]

[ NOTA 2: Come è emerso nei commenti, in questo caso particolare in cui la proprietà è unica e l'oggetto di proprietà è sempre garantito per essere presente (ovvero il membro di dati interno devicenon sarà mai nullptr), la funzione getDevice()potrebbe (e forse dovrebbe) restituisce un riferimento anziché un puntatore. Mentre questo è vero, ho deciso di tornare un puntatore prime qui perché volevo dire che questo è un breve risposta che si potrebbe generalizzare al caso in cui devicepotrebbe essere nullptr, e per dimostrare che i puntatori prime sono OK finché non li si usa per gestione manuale della memoria. - FINE NOTA 2 ]


La situazione è radicalmente diversa, ovviamente, se il tuo Settingsoggetto non deve avere la proprietà esclusiva del dispositivo. Questo potrebbe essere il caso, ad esempio, se la distruzione Settingsdell'oggetto non dovesse implicare anche la distruzione Devicedell'oggetto appuntito .

Questo è qualcosa che solo tu come designer del tuo programma puoi dire; dall'esempio che fornisci, è difficile per me dire se è così o no.

Per aiutarti a capirlo, potresti chiederti se ci sono altri oggetti oltre a quelli Settingsche hanno il diritto di mantenere l' Deviceoggetto in vita fintanto che tengono un puntatore su di esso, invece di essere solo osservatori passivi. In tal caso, è necessaria una politica di proprietà condivisa , che è ciò che std::shared_ptroffre:

#include <memory>

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> const& d) {
        device = d;
    }

    std::shared_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device = std::make_shared<Device>();
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Si noti che si weak_ptrtratta di un puntatore osservatore , non di un puntatore proprietario - in altre parole, non mantiene in vita l'oggetto appuntito se tutti gli altri puntatori proprietari dell'oggetto puntato escono dall'ambito.

Il vantaggio di weak_ptroltre un normale puntatore raw è che puoi tranquillamente dire se weak_ptrè penzolante o meno (cioè se sta puntando a un oggetto valido o se l'oggetto originariamente indicato è stato distrutto). Questo può essere fatto chiamando la expired()funzione membro weak_ptrsull'oggetto.


4
@LKK: Sì, corretto. A weak_ptrè sempre un'alternativa ai puntatori di osservazione grezzi. In un certo senso è più sicuro, perché potresti verificare se è penzolante prima di dereferenziarlo, ma ha anche un po 'di spese generali. Se puoi facilmente garantire che non stai per dereferenziare un puntatore penzolante, allora dovresti stare bene con l'osservazione di puntatori grezzi
Andy Prowl

6
Nel primo caso sarebbe probabilmente anche meglio lasciare getDevice()un riferimento, non è vero? Quindi il chiamante non dovrebbe controllare nullptr.
Oggetto

5
@chico: non sono sicuro di cosa intendi. auto myDevice = settings.getDevice()creerà una nuova istanza di tipo Devicechiamata myDevicee la copierà da quella a cui fa riferimento il riferimento che getDevice()restituisce. Se vuoi myDeviceessere un riferimento, devi farlo auto& myDevice = settings.getDevice(). Quindi, a meno che non mi manchi qualcosa, siamo di nuovo nella stessa situazione che abbiamo avuto senza usare auto.
Andy Prowl,

2
@Purrformance: Perché non vuoi dare via la proprietà dell'oggetto - consegnare un modificabile unique_ptra un client apre la possibilità che il client si sposterà da esso, acquisendo così la proprietà e lasciandoti con un puntatore nullo (unico).
Andy Prowl,

7
@Purrformance: mentre ciò impedirebbe a un cliente di spostarsi (a meno che il cliente non sia uno scienziato pazzo appassionato di const_casts), personalmente non lo farei. Espone un dettaglio di implementazione, cioè il fatto che la proprietà è unica e realizzata attraverso a unique_ptr. Vedo le cose in questo modo: se si desidera / è necessario passare / restituire la proprietà, passare / restituire un puntatore intelligente ( unique_ptro shared_ptr, a seconda del tipo di proprietà). Se non si desidera / è necessario passare / restituire la proprietà, utilizzare un constpuntatore o un riferimento (adeguatamente qualificato), principalmente a seconda che l'argomento possa essere nullo o meno.
Andy Prowl,

0
class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(const std::shared_ptr<Device>& device) : device(device) {

    }

    const std::shared_ptr<Device>& getDevice() {
        return device;
    }
};

int main()
{
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice(settings.getDevice());
    // do something with myDevice...
    return 0;
}

week_ptrviene utilizzato solo per i loop di riferimento. Il grafico delle dipendenze deve essere un grafico indiretto aciclico. Nei puntatori condivisi ci sono 2 conteggi di riferimento: 1 per shared_ptrs e 1 per tutti i puntatori ( shared_ptre weak_ptr). Quando tutti shared_ptri messaggi vengono rimossi, il puntatore viene eliminato. Quando è necessario il puntatore da weak_ptr, lockdovrebbe essere usato per ottenere il puntatore, se esiste.


Quindi, se capisco correttamente la tua risposta, i puntatori intelligenti sostituiscono i puntatori non elaborati, ma non necessariamente i riferimenti?
michaelk,

Ci sono effettivamente due conteggi di riferimento in a shared_ptr? Puoi spiegare perché? Per quanto ho capito, weak_ptrnon deve essere contato perché crea semplicemente un nuovo shared_ptrquando si opera sull'oggetto (se l'oggetto sottostante esiste ancora).
Björn Pollex,

@ BjörnPollex: ho creato un breve esempio per te: link . Non ho implementato tutto solo i costruttori di copie e lock. la versione boost è anche thread-safe sul conteggio dei riferimenti ( deleteviene chiamata una sola volta).
Naszta,

@Naszta: il tuo esempio mostra che è possibile implementarlo usando due conteggi di riferimento, ma la tua risposta suggerisce che questo è necessario , cosa che non credo. Potresti chiarire questo nella tua risposta?
Björn Pollex,

1
@ BjörnPollex, per weak_ptr::lock()sapere se l'oggetto è scaduto deve ispezionare il "blocco di controllo" che contiene il primo conteggio di riferimento e puntatore all'oggetto, quindi il blocco di controllo non deve essere distrutto mentre ci sono weak_ptrancora oggetti in uso, quindi il numero di weak_ptroggetti deve essere tracciato, che è ciò che fa il secondo conteggio di riferimento. L'oggetto viene distrutto quando il primo conteggio di riferimento scende a zero, il blocco di controllo viene distrutto quando il secondo conteggio di riferimento scende a zero.
Jonathan Wakely
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.