C'è qualche utilità per unique_ptr con array?


238

std::unique_ptr supporta gli array, ad esempio:

std::unique_ptr<int[]> p(new int[10]);

ma è necessario? probabilmente è più comodo da usare std::vectoro std::array.

Trovi utile per quel costrutto?


6
Per completezza, dovrei sottolineare che non c'è std::shared_ptr<T[]>, ma ci dovrebbe essere, e probabilmente sarà in C ++ 14 se qualcuno potrebbe essere disturbato a scrivere una proposta. Nel frattempo, c'è sempre boost::shared_array.
Pseudonimo del

13
std::shared_ptr<T []> è ora in c ++ 17.
陳 力

Puoi trovare diversi modi per fare qualsiasi cosa su un computer. Questo costrutto è utile, soprattutto in un percorso attivo, poiché elimina l'overhead delle operazioni del contenitore se si sa esattamente come indirizzare l'array. Inoltre, crea array di caratteri senza alcun dubbio di archiviazione contigua.
Kevin il

Risposte:


256

Alcune persone non hanno il lusso di usare std::vector, anche con gli allocatori. Alcune persone hanno bisogno di un array di dimensioni dinamiche, quindi std::arrayè fuori. E alcune persone ottengono i loro array da altri codici noti per restituire un array; e quel codice non verrà riscritto per restituire un vectoro qualcosa del genere.

Consentendo unique_ptr<T[]>, soddisfate tali esigenze.

In breve, si usa unique_ptr<T[]>quando è necessario . Quando le alternative semplicemente non funzioneranno per te. È uno strumento di ultima istanza.


27
@NoSenseEtAl: Non sono sicuro di quale parte di "ad alcune persone non è permesso farlo" ti sfugge. Alcuni progetti hanno requisiti molto specifici e tra questi potrebbe esserci "non puoi usare vector". Puoi discutere se si tratta di requisiti ragionevoli o meno, ma non puoi negare che esistano .
Nicol Bolas,

21
Non vi è alcun motivo al mondo per cui qualcuno non sarebbe in grado di utilizzare std::vectorse fosse possibile std::unique_ptr.
Miglia rotta

66
ecco un motivo per non usare vector: sizeof (std :: vector <char>) == 24; sizeof (std :: unique_ptr <char []>) == 8
Arvid

13
@DanNissenbaum Questi progetti esistono. Alcune industrie che sono sottoposte a scrupolose verifiche, come ad esempio l'aviazione o la difesa, la biblioteca standard è vietata perché è difficile verificare e dimostrare che è corretta per qualunque organo di governo stabilisca i regolamenti. Potresti sostenere che la libreria standard è ben testata e che sarei d'accordo con te, ma io e te non facciamo le regole.
Emily L.,

16
@DanNissenbaum Inoltre, alcuni sistemi in tempo reale non sono autorizzati a utilizzare l'allocazione dinamica della memoria poiché il ritardo causato da una chiamata di sistema potrebbe non essere teoricamente limitato e non è possibile dimostrare il comportamento in tempo reale del programma. Oppure il limite potrebbe essere troppo grande, il che infrange il limite WCET. Anche se non applicabile qui, dal momento che non userebbero unique_ptrneanche questo, ma quel tipo di progetti esiste davvero.
Emily L.,

124

Ci sono dei compromessi e scegli la soluzione che corrisponde a ciò che desideri. In cima alla mia testa:

Dimensione iniziale

  • vectore unique_ptr<T[]>consentire la dimensione da specificare in fase di esecuzione
  • array consente solo la dimensione da specificare al momento della compilazione

Ridimensionamento

  • arraye unique_ptr<T[]>non consentire il ridimensionamento
  • vector fa

Conservazione

  • vectore unique_ptr<T[]>archiviare i dati all'esterno dell'oggetto (in genere nell'heap)
  • array memorizza i dati direttamente nell'oggetto

copiatura

  • arraye vectorconsentire la copia
  • unique_ptr<T[]> non consente la copia

Swap / mossa

  • vectore unique_ptr<T[]>avere O (1) swapoperazioni di tempo e spostamento
  • arrayha O (n) swapoperazioni di tempo e spostamento, dove n è il numero di elementi nell'array

Annullamento puntatore / riferimento / iteratore

  • array assicura che puntatori, riferimenti e iteratori non vengano mai invalidati mentre l'oggetto è attivo, anche su swap()
  • unique_ptr<T[]>non ha iteratori; i puntatori e i riferimenti vengono invalidati solo swap()mentre l'oggetto è attivo. (Dopo lo scambio, i puntatori puntano verso l'array con cui si è scambiato, quindi sono ancora "validi" in tal senso.)
  • vector può invalidare puntatori, riferimenti e iteratori su qualsiasi riallocazione (e fornisce alcune garanzie che la riallocazione può avvenire solo su determinate operazioni).

Compatibilità con concetti e algoritmi

  • arraye vectorsono entrambi contenitori
  • unique_ptr<T[]> non è un contenitore

Devo ammetterlo, questa sembra un'opportunità per alcuni refactoring con un design basato su criteri.


1
Non sono sicuro di aver capito cosa intendi nel contesto di invalidazione del puntatore . Si tratta di puntatori agli oggetti stessi o puntatori agli elementi? O qualcos'altro? Che tipo di garanzia ottieni da un array che non ricevi da un vettore?
jogojapan,

3
Supponiamo di avere un iteratore, un puntatore o un riferimento a un elemento di a vector. Quindi si aumenta la dimensione o la capacità di vectortale da forzare una riallocazione. Quindi quell'iteratore, puntatore o riferimento non punta più a quell'elemento di vector. Questo è ciò che intendiamo per "invalidazione". Questo problema non si verifica arrayperché non esiste una "riallocazione". In realtà, ho appena notato un dettaglio con quello, e l'ho modificato per adattarlo.
Pseudonimo del

1
Ok, non ci può essere invalidazione a causa della riallocazione in un array o unique_ptr<T[]>perché non c'è riallocazione. Ma ovviamente, quando l'array non rientra nell'ambito, i puntatori a elementi specifici verranno comunque invalidati.
jogojapan

Sì, tutte le scommesse sono disattivate se l'oggetto non è più attivo.
Pseudonimo del

1
@rubenvb Certo che puoi, ma non puoi (dire) usare direttamente il range-based per i loop. Per inciso, a differenza di un normale T[], le dimensioni (o informazioni equivalenti) devono essere sospese da qualche parte per operator delete[]distruggere correttamente gli elementi dell'array. Sarebbe bello se il programmatore avesse accesso a quello.
Pseudonimo del

73

Uno dei motivi per cui potresti usare a unique_ptrè se non vuoi pagare il costo di runtime dell'inizializzazione del valore dell'array.

std::vector<char> vec(1000000); // allocates AND value-initializes 1000000 chars

std::unique_ptr<char[]> p(new char[1000000]); // allocates storage for 1000000 chars

Il std::vectorcostruttore e std::vector::resize()inizializzerà il valore, Tma newnon lo farà se Tè un POD.

Vedi Oggetti inizializzati dal valore in C ++ 11 e costruttore std :: vector

Nota che vector::reservenon è un'alternativa qui: l' accesso al puntatore non elaborato dopo std :: vector :: reserve è sicuro?

È la stessa ragione per cui un programmatore C potrebbe scegliere malloc sopra calloc.


Ma questa ragione non è l'unica soluzione .
Ruslan,

@Ruslan Nella soluzione collegata gli elementi dell'array dinamico sono ancora inizializzati dal valore, ma l'inizializzazione del valore non fa nulla. Concordo sul fatto che un ottimizzatore che non riesce a rendersi conto che non si può implementare nulla senza 1000000 volte non vale un centesimo, ma si potrebbe preferire di non dipendere affatto da questa ottimizzazione.
Marc van Leeuwen,

ancora un'altra possibilità è quella di fornire a std::vectorun allocatore personalizzato che eviti la costruzione di tipi che sono std::is_trivially_default_constructiblee la distruzione di oggetti che sono std::is_trivially_destructible, sebbene ciò violi rigorosamente lo standard C ++ (poiché tali tipi non sono inizializzati di default).
Walter,

Inoltre std::unique_ptrnon fornisce alcun controllo associato contrario a molte std::vectorimplementazioni.
diapir il

@diapir Non si tratta dell'implementazione: std::vectorè richiesto dallo Standard per verificare i limiti .at(). Immagino che intendessi dire che alcune implementazioni hanno anche modalità di debug che eseguiranno il check-in .operator[], ma lo considero inutile per scrivere un buon codice portatile.
underscore_d

30

Un std::vectorpuò essere copiato in giro, mentre unique_ptr<int[]>consente di esprimere la proprietà unica dell'array. std::arrayd'altra parte, richiede che la dimensione sia determinata in fase di compilazione, il che può essere impossibile in alcune situazioni.


2
Solo perché qualcosa può essere copiato non significa che debba esserlo.
Nicol Bolas,

4
@NicolBolas: non capisco. Si potrebbe desiderare di impedirlo per lo stesso motivo per cui si dovrebbe usare unique_ptrinvece di shared_ptr. Mi sto perdendo qualcosa?
Andy Prowl,

4
unique_ptrfa molto di più che prevenire un uso improprio accidentale. È anche più piccolo e più basso rispetto a shared_ptr. Il punto è che, sebbene sia bello avere una semantica in una classe che previene "l'abuso", questa non è l'unica ragione per usare un tipo particolare. Ed vectorè molto più utile come memoria di array che unique_ptr<T[]>, se non altro per il fatto che ha una dimensione .
Nicol Bolas,

3
Pensavo di aver chiarito il punto: ci sono altri motivi per usare un tipo particolare oltre a quello. Proprio come ci sono motivi per preferire vectorsopra unique_ptr<T[]>, ove possibile, invece di dire: "non è possibile copiarlo" e quindi scegliere unique_ptr<T[]>quando non si vuole copie. Impedire a qualcuno di fare la cosa sbagliata non è necessariamente il motivo più importante per scegliere una classe.
Nicol Bolas,

8
std::vectorha un overhead maggiore di un std::unique_ptr- usa ~ 3 puntatori invece di ~ 1. std::unique_ptrblocca la costruzione della copia ma abilita la costruzione dello spostamento, che se semanticamente i dati con cui si sta lavorando possono essere spostati ma non copiati, infetta il classcontenuto dei dati. Fare un'operazione su dati non validi in realtà peggiora la classe del contenitore e "semplicemente non usarla" non lava via tutti i peccati. Dover mettere ogni tua istanza std::vectorin una classe in cui disabiliti manualmente moveè un mal di testa. std::unique_ptr<std::array>ha un size.
Yakk - Adam Nevraumont

22

Scott Meyers ha questo da dire in Effective Modern C ++

L'esistenza di std::unique_ptrper le matrici devono considerarsi di interesse intellettuale a voi, perché std::array, std::vector, std::stringsono praticamente sempre le scelte migliori strutture dati di array grezzi. Sull'unica situazione che posso immaginare quando std::unique_ptr<T[]>potrebbe avere senso sarebbe quando si utilizza un'API di tipo C che restituisce un puntatore non elaborato a un array di heap di cui si assume la proprietà.

Penso che la risposta di Charles Salvia sia pertinente: std::unique_ptr<T[]>è l'unico modo per inizializzare un array vuoto le cui dimensioni non sono note al momento della compilazione. Cosa direbbe Scott Meyers a proposito di questa motivazione per l'utilizzo std::unique_ptr<T[]>?


4
Sembra che semplicemente non abbia immaginato alcuni casi d'uso, vale a dire un buffer le cui dimensioni sono fisse ma sconosciute al momento della compilazione e / o un buffer per il quale non consentiamo copie. C'è anche efficienza come possibile ragione per preferirla a vector stackoverflow.com/a/24852984/2436175 .
Antonio,

17

Contrariamente a std::vectore std::array, std::unique_ptrpuò possedere un puntatore NULL.
Ciò è utile quando si lavora con API C che prevedono un array o NULL:

void legacy_func(const int *array_or_null);

void some_func() {    
    std::unique_ptr<int[]> ptr;
    if (some_condition) {
        ptr.reset(new int[10]);
    }

    legacy_func(ptr.get());
}

10

Ho usato unique_ptr<char[]>per implementare un pool di memoria preallocato utilizzato in un motore di gioco. L'idea è di fornire pool di memoria preallocati utilizzati al posto di allocazioni dinamiche per restituire i risultati delle richieste di collisione e altre cose come la fisica delle particelle senza dover allocare / liberare memoria su ciascun frame. È abbastanza conveniente per questo tipo di scenari in cui sono necessari pool di memoria per allocare oggetti con durata di vita limitata (in genere uno, 2 o 3 frame) che non richiedono una logica di distruzione (solo deallocazione della memoria).


9

Un modello comune può essere trovato in alcune chiamate API Win32 di Windows , in cui l'uso std::unique_ptr<T[]>può tornare utile, ad esempio quando non si sa esattamente quanto dovrebbe essere grande un buffer di output quando si chiama qualche API Win32 (che scriverà alcuni dati all'interno quel buffer):

// Buffer dynamically allocated by the caller, and filled by some Win32 API function.
// (Allocation will be made inside the 'while' loop below.)
std::unique_ptr<BYTE[]> buffer;

// Buffer length, in bytes.
// Initialize with some initial length that you expect to succeed at the first API call.
UINT32 bufferLength = /* ... */;

LONG returnCode = ERROR_INSUFFICIENT_BUFFER;
while (returnCode == ERROR_INSUFFICIENT_BUFFER)
{
    // Allocate buffer of specified length
    buffer.reset( BYTE[bufferLength] );
    //        
    // Or, in C++14, could use make_unique() instead, e.g.
    //
    // buffer = std::make_unique<BYTE[]>(bufferLength);
    //

    //
    // Call some Win32 API.
    //
    // If the size of the buffer (stored in 'bufferLength') is not big enough,
    // the API will return ERROR_INSUFFICIENT_BUFFER, and the required size
    // in the [in, out] parameter 'bufferLength'.
    // In that case, there will be another try in the next loop iteration
    // (with the allocation of a bigger buffer).
    //
    // Else, we'll exit the while loop body, and there will be either a failure
    // different from ERROR_INSUFFICIENT_BUFFER, or the call will be successful
    // and the required information will be available in the buffer.
    //
    returnCode = ::SomeApiCall(inParam1, inParam2, inParam3, 
                               &bufferLength, // size of output buffer
                               buffer.get(),  // output buffer pointer
                               &outParam1, &outParam2);
}

if (Failed(returnCode))
{
    // Handle failure, or throw exception, etc.
    ...
}

// All right!
// Do some processing with the returned information...
...

Potresti semplicemente usare std::vector<char>in questi casi.
Arthur Tacca,

@ArthurTacca - ... se non ti dispiace che il compilatore inizializzi tutti i caratteri nel buffer su 0 uno a uno.
TED

9

Ho dovuto affrontare un caso in cui dovevo usare std::unique_ptr<bool[]>, che si trovava nella libreria HDF5 (una libreria per un'efficiente memorizzazione dei dati binari, usata molto nella scienza). Alcuni compilatori (Visual Studio 2015 nel mio caso) forniscono la compressione distd::vector<bool> (usando 8 bool in ogni byte), che è una catastrofe per qualcosa come HDF5, a cui non interessa quella compressione. Constd::vector<bool> , HDF5 stava finalmente leggendo la spazzatura a causa di quella compressione.

Indovina chi era lì per il salvataggio, in un caso in cui std::vectornon ha funzionato, e ho dovuto allocare un array dinamico in modo pulito? :-)


9

In breve: è di gran lunga il più efficiente in termini di memoria.

A std::stringviene fornito con un puntatore, una lunghezza e un buffer di "ottimizzazione delle stringhe corte". Ma la mia situazione è che ho bisogno di memorizzare una stringa che è quasi sempre vuota, in una struttura di cui ho centinaia di migliaia. In C, userei solo char *, e sarebbe null per la maggior parte del tempo. Che funziona anche per C ++, tranne per il fatto che a char *non ha distruttori e non sa di cancellarsi. Al contrario, a std::unique_ptr<char[]>si cancellerà da solo quando esce dal campo di applicazione. Un vuoto std::stringoccupa 32 byte, ma un vuotostd::unique_ptr<char[]> occupa 8 byte, ovvero esattamente la dimensione del suo puntatore.

Il più grande svantaggio è che, ogni volta che voglio sapere la lunghezza della stringa, devo fare appello strlen.


3

Per rispondere alle persone che pensano di "dover" usare vectorinvece di unique_ptrun caso nella programmazione CUDA su GPU quando si alloca memoria in Dispositivo, è necessario scegliere un array di puntatori (con cudaMalloc). Quindi, quando si recuperano questi dati in Host, è necessario andare di nuovo per un puntatore e unique_ptrva bene per gestirlo facilmente. Il costo aggiuntivo della conversione double*in vector<double>non è necessario e porta a una perdita di perf.


3

Un motivo in più per consentire e utilizzare std::unique_ptr<T[]>, che finora non è stato menzionato nelle risposte: consente di dichiarare in avanti il ​​tipo di elemento array.

Ciò è utile quando si desidera ridurre al minimo le #includeistruzioni concatenate nelle intestazioni (per ottimizzare le prestazioni della build).

Per esempio -

MyClass.h:

class ALargeAndComplicatedClassWithLotsOfDependencies;

class MyClass {
   ...
private:
   std::unique_ptr<ALargeAndComplicatedClassWithLotsOfDependencies[]> m_InternalArray;
};

MyClass.cpp:

#include "myclass.h"
#include "ALargeAndComplicatedClassWithLotsOfDependencies.h"

// MyClass implementation goes here

Con la struttura del codice sopra, chiunque può #include "myclass.h"e utilizzare MyClass, senza dover includere le dipendenze di implementazione interne richieste da MyClass::m_InternalArray.

Se m_InternalArrayinvece fosse dichiarato rispettivamente come a std::array<ALargeAndComplicatedClassWithLotsOfDependencies>, o a std::vector<...>- il risultato sarebbe un tentativo di utilizzo di un tipo incompleto, che è un errore in fase di compilazione.


Per questo particolare caso d'uso, opterei per il modello Pimpl per interrompere la dipendenza: se viene utilizzato solo privatamente, la definizione può essere rinviata fino all'implementazione dei metodi di classe; se è usato pubblicamente, gli utenti della classe avrebbero già dovuto avere le conoscenze concrete class ALargeAndComplicatedClassWithLotsOfDependencies. Quindi logicamente non dovresti imbatterti in tali scenari.

3

Non posso essere abbastanza in disaccordo con lo spirito della risposta accettata. "Uno strumento di ultima istanza"? Lontano da esso!

Per come la vedo io, una delle caratteristiche più forti di C ++ rispetto a C e ad altri linguaggi simili è la capacità di esprimere i vincoli in modo che possano essere controllati in fase di compilazione e che sia possibile prevenire abusi accidentali. Quindi, quando si progetta una struttura, chiediti quali operazioni dovrebbe consentire. Tutti gli altri usi dovrebbero essere vietati, ed è meglio se tali restrizioni possono essere implementate staticamente (al momento della compilazione) in modo che l'uso improprio comporti un errore di compilazione.

Quindi, quando si ha bisogno di un array, le risposte alle seguenti domande specificano il suo comportamento: 1. Le sue dimensioni sono a) dinamiche in fase di esecuzione o b) statiche, ma note solo in fase di esecuzione o c) statiche e note al momento della compilazione? 2. L'array può essere allocato nello stack o no?

E in base alle risposte, questo è quello che vedo come la migliore struttura di dati per un tale array:

       Dynamic     |   Runtime static   |         Static
Stack std::vector      unique_ptr<T[]>          std::array
Heap  std::vector      unique_ptr<T[]>     unique_ptr<std::array>

Sì, penso unique_ptr<std::array> dovrei anche essere considerato, e nessuno dei due è uno strumento di ultima istanza. Pensa solo a ciò che si adatta meglio al tuo algoritmo.

Tutti questi sono compatibili con le API C semplici tramite il puntatore non elaborato all'array di dati ( vector.data()/ array.data()/ uniquePtr.get()).

PS Oltre alle considerazioni di cui sopra, ce n'è anche una di proprietà: std::arraye std::vectorha una semantica di valore (ha un supporto nativo per la copia e il passaggio per valore), mentre unique_ptr<T[]>può essere spostata (impone la proprietà singola). Entrambi possono essere utili in diversi scenari. Al contrario, gli array statici semplici ( int[N]) e gli array dinamici semplici ( new int[10]) non offrono nessuno dei due e quindi dovrebbero essere evitati, se possibile, il che dovrebbe essere possibile nella stragrande maggioranza dei casi. Se ciò non bastasse, anche le semplici matrici dinamiche non offrono alcun modo per interrogarne le dimensioni: un'opportunità in più per corruzione della memoria e falle di sicurezza.


2

Potrebbero essere la risposta più giusta possibile quando si arriva a colpire un singolo puntatore attraverso un'API esistente (messaggio di finestra think o parametri di callback relativi al threading) che hanno una certa misura della vita dopo essere stati "catturati" dall'altra parte del tratteggio, ma che non è correlato al codice chiamante:

unique_ptr<byte[]> data = get_some_data();

threadpool->post_work([](void* param) { do_a_thing(unique_ptr<byte[]>((byte*)param)); },
                      data.release());

Vogliamo tutti che le cose siano belle per noi. C ++ è per le altre volte.


2

unique_ptr<char[]>può essere usato dove vuoi le prestazioni di C e la convenienza di C ++. Considera di dover operare su milioni (ok, miliardi se non ti fidi ancora) di stringhe. La memorizzazione di ciascuno di essi in un oggetto separato stringo vector<char>sarebbe un disastro per le routine di gestione della memoria (heap). Soprattutto se è necessario allocare ed eliminare più stringhe diverse più volte.

Tuttavia, è possibile allocare un singolo buffer per la memorizzazione di tante stringhe. Non ti piacerebbe char* buffer = (char*)malloc(total_size);per ovvi motivi (se non ovvio, cerca "why use smart ptrs"). Preferirestiunique_ptr<char[]> buffer(new char[total_size]);

Per analogia, le stesse considerazioni relative a prestazioni e convenienza si applicano ai non chardati (considerare milioni di vettori / matrici / oggetti).


Uno non li mette tutti in uno grande vector<char>? La risposta, suppongo, è perché saranno inizializzati a zero quando si crea il buffer, mentre non lo saranno se si utilizza unique_ptr<char[]>. Ma questo nugget chiave manca nella tua risposta.
Arthur Tacca,

2
  • È necessario che la struttura contenga solo un puntatore per motivi di compatibilità binaria.
  • È necessario interfacciarsi con un'API che restituisce memoria allocata new[]
  • La tua azienda o progetto ha una regola generale contro l'utilizzo std::vector, ad esempio, per impedire ai programmatori negligenti di introdurre accidentalmente copie
  • Volete impedire ai programmatori incuranti di introdurre accidentalmente copie in questo caso.

Esiste una regola generale secondo cui i contenitori C ++ devono essere preferiti al rolling-your-own con puntatori. È una regola generale; ha delle eccezioni. C'è più; questi sono solo esempi.


0

Se hai bisogno di un array dinamico di oggetti che non sono costruibili per la copia, allora un puntatore intelligente a un array è la strada da percorrere. Ad esempio, cosa succede se hai bisogno di una matrice di atomica.

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.