Puntatori intelligenti (C++ moderno)

Nella programmazione C++ moderna, la libreria standard include puntatori intelligenti, che vengono usati per garantire che i programmi siano privi di memoria e perdite di risorse e siano sicuri dalle eccezioni.

Utilizzi per i puntatori intelligenti

I puntatori intelligenti sono definiti nel namespace std nel file header <memory>. Sono cruciali per l'idioma di programmazione Resource Acquisition Is Initialization(RAII). L'obiettivo principale di questo linguaggio è garantire che l'acquisizione delle risorse si verifichi contemporaneamente all'inizializzazione dell'oggetto. Tutte le risorse per l'oggetto vengono create e pronte in una riga di codice.

In pratica, il principio principale di RAII consiste nell'assegnare la proprietà di qualsiasi risorsa allocata all'heap a un oggetto allocato dallo stack il cui distruttore contiene il codice per eliminare o liberare la risorsa e anche qualsiasi codice di pulizia associato. Tali oggetti includono memoria allocata dinamicamente o handle di oggetti di sistema.

Nella maggior parte dei casi, quando si inizializza un puntatore raw o un handle di risorsa in modo che faccia riferimento a una risorsa effettiva, passare immediatamente il puntatore a uno smart pointer. Nel C++ moderno, i puntatori grezzi vengono usati solo in piccoli blocchi di codice con ambito limitato, loop o funzioni di supporto in cui le prestazioni sono critiche e non vi è alcuna possibilità di confusione riguardo alla proprietà.

Nell'esempio seguente viene fatto un confronto tra una dichiarazione di puntatore non elaborato e una dichiarazione di puntatore intelligente.

void UseRawPointer()
{
    // Using a raw pointer -- not recommended.
    Song* pSong = new Song(L"Nothing on You", L"Bruno Mars"); 

    // Use pSong...

    // Don't forget to delete!
    delete pSong;   
}

void UseSmartPointer()
{
    // Declare a smart pointer on stack and pass it the raw pointer.
    unique_ptr<Song> song2(new Song(L"Nothing on You", L"Bruno Mars"));

    // Use song2...
    wstring s = song2->duration_;
    //...

} // song2 is deleted automatically here.

Come illustrato nell'esempio, un puntatore intelligente è un modello di classe dichiarato nello stack e inizializzato tramite un puntatore non elaborato che punta a un oggetto allocato dall'heap. Dopo essere stato inizializzato, il puntatore intelligente assume la proprietà del puntatore grezzo. Questo approccio significa che il puntatore intelligente è responsabile dell'eliminazione della memoria specificata dal puntatore non elaborato.

Il distruttore del puntatore intelligente contiene la chiamata a delete. Poiché il puntatore intelligente viene dichiarato sullo stack, il suo distruttore viene invocato quando il puntatore intelligente non è più nell'ambito di validità. Viene richiamato anche se viene generata un'eccezione in un punto più alto dello stack.

Accedere al puntatore incapsulato usando gli operatori del puntatore familiari: -> e *. La classe smart pointer sovraccarica questi operatori per restituire il puntatore raw incapsulato.

Il linguaggio del puntatore intelligente C++ è simile alla creazione di oggetti in linguaggi come C#. Si crea l'oggetto e quindi si lascia che il sistema si occupi dell'eliminazione al momento corretto. La differenza è che nessun Garbage Collector separato viene eseguito in background. La memoria viene gestita tramite le regole di ambito C++ standard in modo che l'ambiente di runtime sia più veloce ed efficiente.

Importante

Creare sempre puntatori intelligenti su una riga di codice separata, mai in un elenco di parametri. Questo approccio impedisce una perdita di risorse sottile a causa di determinate regole di allocazione dell'elenco di parametri.

Nell'esempio seguente viene illustrato come usare un unique_ptr tipo di puntatore intelligente dalla libreria standard C++ per incapsulare un puntatore a un oggetto di grandi dimensioni.

class LargeObject
{
public:
    void DoSomething(){}
};

void ProcessLargeObject(const LargeObject& lo){}
void SmartPointerDemo()
{    
    // Create the object and pass it to a smart pointer
    std::unique_ptr<LargeObject> pLarge(new LargeObject());

    //Call a method on the object
    pLarge->DoSomething();

    // Pass a reference to a method.
    ProcessLargeObject(*pLarge);

} //pLarge is deleted automatically when function block goes out of scope.

Di seguito vengono illustrati i passaggi essenziali per utilizzare i puntatori intelligenti.

  1. Dichiarare il puntatore intelligente come variabile automatica (locale). Non usare l'espressione new o malloc sul puntatore intelligente stesso.

  2. Nel parametro di tipo specificare il tipo di riferimento del puntatore incapsulato.

  3. Passare un puntatore grezzo a un oggetto new nel costruttore dello smart pointer. Alcune funzioni di utilità o costruttori di puntatori intelligenti eseguono automaticamente questa operazione.

  4. Utilizzare gli operatori -> e * sovraccaricati per accedere all'oggetto.

  5. Consentire al puntatore intelligente di eliminare l'oggetto.

I puntatori intelligenti sono progettati per essere estremamente efficaci sia in termini di memoria che di prestazioni. Ad esempio, l'unico membro dati in unique_ptr è il puntatore incapsulato. Questo significa che unique_ptr corrisponde esattamente alla stessa dimensione del puntatore, ovvero quattro byte o otto byte. Accedere al puntatore incapsulato usando gli operatori sovraccaricati * e -> dello smart pointer non è significativamente più lento dell'accesso diretto ai puntatori grezzi.

I puntatori intelligenti dispongono di funzioni membro proprie, alle quali si accede usando la notazione con il punto. Ad esempio, alcuni puntatori intelligenti della libreria standard C++ hanno una reset funzione membro che rilascia la proprietà del puntatore. Questo comportamento è utile quando si desidera liberare la memoria posseduta dal puntatore intelligente prima che esso esca dal proprio ambito di validità, come illustrato nell'esempio seguente.

void SmartPointerDemo2()
{
    // Create the object and pass it to a smart pointer
    std::unique_ptr<LargeObject> pLarge(new LargeObject());

    //Call a method on the object
    pLarge->DoSomething();

    // Free the memory before we exit function block.
    pLarge.reset();

    // Do some other work...

}

I puntatori intelligenti consentono in genere di accedere direttamente al relativo puntatore non elaborato. I puntatori intelligenti della libreria standard C++ dispongono di una funzione membro get a questo scopo. CComPtr ha un membro della classe pubblica p . Fornendo l'accesso diretto al puntatore sottostante, è possibile usare il puntatore intelligente per gestire la memoria nel codice e passare comunque il puntatore non elaborato al codice che non supporta puntatori intelligenti.

void SmartPointerDemo4()
{
    // Create the object and pass it to a smart pointer
    std::unique_ptr<LargeObject> pLarge(new LargeObject());

    //Call a method on the object
    pLarge->DoSomething();

    // Pass raw pointer to a legacy API
    LegacyLargeObjectFunction(pLarge.get());    
}

Tipi di puntatori intelligenti

Nella sezione seguente vengono riepilogati i diversi tipi di puntatori intelligenti disponibili nell'ambiente di programmazione Windows e ne viene descritto l'utilizzo.

Puntatori intelligenti della libreria standard C++

Utilizzare questi puntatori intelligenti come scelta principale per incapsulare puntatori a semplici oggetti C++ tradizionali (POCO, plain old C++ objects).

  • unique_ptr

    Consente un solo proprietario per il puntatore sottostante. Utilizzalo come scelta predefinita per POCO, a meno che tu non sappia con certezza che ti serve un shared_ptr. Può essere spostato a un nuovo proprietario, ma non copiato o condiviso. Sostituisce auto_ptr, che è deprecato. Confrontare con boost::scoped_ptr.

    unique_ptr è piccolo ed efficiente. La dimensione è un puntatore e supporta riferimenti rvalue per l'inserimento e il recupero rapidi dalle raccolte di librerie standard C++. File di intestazione: <memory>. Per altre informazioni, vedere Procedura: Creare e usare istanze di unique_ptr e classe unique_ptr.

  • shared_ptr

    Puntatore intelligente con conteggio dei riferimenti. Usare quando si vuole assegnare un puntatore non elaborato a più proprietari. Ad esempio, è possibile restituire una copia di un puntatore da un contenitore, ma mantenere anche l'originale. Il puntatore raw non viene eliminato finché tutti i proprietari di shared_ptr non escono dall'ambito o non rinunciano in altro modo alla proprietà.

    La dimensione è pari a due puntatori: uno per l'oggetto e uno per il blocco di controllo condiviso che contiene il contatore di riferimenti. File di intestazione: <memory>. Per altre informazioni, vedere Procedura: Creare e usare istanze di shared_ptr e classe shared_ptr.

  • weak_ptr

    Puntatore intelligente specifico da utilizzare con shared_ptr. Un weak_ptr fornisce l'accesso a un oggetto posseduto da una o più istanze di shared_ptr, ma non partecipa al conteggio dei riferimenti. Usare quando si vuole osservare un oggetto, ma non è necessario che rimanga attivo. Necessario in alcuni casi per interrompere i riferimenti circolari tra istanze di shared_ptr.

    File di intestazione: <memory>. Per altre informazioni, vedere Procedura: Creare e usare istanze di weak_ptr e classe weak_ptr.

Puntatori intelligenti per gli oggetti COM (programmazione Classica di Windows)

Quando si utilizzano oggetti COM, racchiudere i puntatori di interfaccia in un tipo appropriato di smart pointer. La libreria ATL (Active Template Library) definisce diversi puntatori intelligenti che assolvono a funzioni diverse. È anche possibile utilizzare il tipo di puntatore intelligente _com_ptr_t, utilizzato dal compilatore durante la creazione di classi wrapper da file TLB. È la scelta migliore se non si desidera includere i file header ATL.

Puntatori intelligenti ATL per oggetti POCO

Oltre ai puntatori intelligenti per gli oggetti COM, ATL definisce anche puntatori intelligenti e raccolte di puntatori intelligenti per semplici oggetti C++ (POCO). Nella programmazione classica Windows questi tipi sono utili alternative alle raccolte di librerie standard C++, soprattutto quando la portabilità del codice non è necessaria o quando non si vogliono combinare i modelli di programmazione della libreria standard C++ e ATL.

  • Classe CAutoPtr

    Puntatore intelligente che applica la proprietà univoca trasferendo la proprietà sulla copia. Paragonabile alla classe std::auto_ptr deprecata.

  • Classe CHeapPtr

    Puntatore intelligente per gli oggetti allocati tramite la funzione C malloc .

  • Classe CAutoVectorPtr

    Puntatore intelligente per le matrici allocate tramite new[].

  • Classe CAutoPtrArray

    Classe che incapsula una matrice di elementi CAutoPtr.

  • Classe CAutoPtrList

    Classe che incapsula metodi per modificare un elenco di nodi CAutoPtr.

Vedi anche