C ++: puntatori intelligenti, puntatori non elaborati, nessun puntatore? [chiuso]


48

Nell'ambito dello sviluppo di giochi in C ++, quali sono i tuoi schemi preferiti per quanto riguarda l'uso dei puntatori (sia esso nessuno, grezzo, mirato, condiviso o comunque tra intelligente e muto)?

Potresti considerare

  • proprietà dell'oggetto
  • facilità d'uso
  • politica di copia
  • alto
  • riferimenti ciclici
  • piattaforma di destinazione
  • utilizzare con contenitori

Risposte:


32

Dopo aver provato vari approcci, oggi mi trovo in linea con la Guida allo stile di Google C ++ :

Se hai davvero bisogno della semantica del puntatore, scoped_ptr è fantastico. Dovresti usare std :: tr1 :: shared_ptr solo in condizioni molto specifiche, come quando gli oggetti devono essere tenuti dai contenitori STL. Non dovresti mai usare auto_ptr. [...]

In generale, preferiamo progettare codice con chiara proprietà dell'oggetto. La proprietà dell'oggetto più chiara si ottiene utilizzando direttamente un oggetto come campo o variabile locale, senza utilizzare affatto i puntatori. [..]

Sebbene non siano raccomandati, i puntatori conteggiati di riferimento sono talvolta il modo più semplice ed elegante per risolvere un problema.


14
Oggi potresti voler usare std :: unique_ptr invece di scoped_ptr.
Klaim,

24

Seguo anche il treno del pensiero "forte proprietà". Mi piace delineare chiaramente che "questa classe possiede questo membro" quando è appropriato.

Uso raramente shared_ptr. Se lo faccio, faccio un uso liberale di weak_ptrogni volta che posso, così posso trattarlo come un handle per l'oggetto invece di aumentare il conteggio dei riferimenti.

Uso scoped_ptrdappertutto. Mostra evidente proprietà. L'unico motivo per cui non faccio solo oggetti del genere è che un membro è perché puoi inoltrarli dichiarandoli se sono in scoped_ptr.

Se ho bisogno di un elenco di oggetti, utilizzo ptr_vector. È più efficiente e ha meno effetti collaterali rispetto all'utilizzo vector<shared_ptr>. Penso che potresti non essere in grado di inoltrare il tipo in ptr_vector (è passato un po '), ma la semantica ne vale la pena secondo me. Fondamentalmente se si rimuove un oggetto dall'elenco, questo viene eliminato automaticamente. Ciò dimostra anche la proprietà evidente.

Se ho bisogno di un riferimento a qualcosa, provo a renderlo un riferimento anziché un puntatore nudo. A volte questo non è pratico (cioè ogni volta che hai bisogno di un riferimento dopo che l'oggetto è stato costruito). In entrambi i casi, i riferimenti mostrano ovviamente che non sei il proprietario dell'oggetto e se segui la semantica dei puntatori condivisi ovunque, i puntatori nudi in genere non causano ulteriore confusione (specialmente se segui una regola "nessuna eliminazione manuale") .

Con questo metodo, un gioco per iPhone su cui ho lavorato è stato in grado di avere una sola deletechiamata, e quello era nel bridge da Obj-C a C ++ che ho scritto.

In generale, sono dell'opinione che la gestione della memoria sia troppo importante per essere lasciata agli umani. Se puoi automatizzare la cancellazione, dovresti. Se l'overhead di shared_ptr è troppo costoso in fase di esecuzione (supponendo che tu abbia disattivato il supporto per il threading, ecc.), Probabilmente dovresti utilizzare qualcos'altro (ad esempio un modello bucket) per ridurre le allocazioni dinamiche.


1
Riepilogo eccellente. Intendi davvero shared_ptr in contrapposizione alla tua menzione di smart_ptr?
jmp97,

Sì, intendevo shared_ptr. Lo aggiusterò.
Tetrad,

10

Usa lo strumento giusto per il lavoro.

Se il tuo programma può generare eccezioni, assicurati che il tuo codice sia a conoscenza delle eccezioni. L'uso di puntatori intelligenti, RAII ed evitare la costruzione in 2 fasi sono buoni punti di partenza.

Se hai riferimenti ciclici senza semantica chiara sulla proprietà, puoi prendere in considerazione l'uso di una libreria di raccolta rifiuti o il refactoring del tuo progetto.

Buone librerie ti permetteranno di codificare il concetto non sul tipo, quindi nella maggior parte dei casi non dovrebbe importare quale tipo di puntatore stai usando oltre ai problemi di gestione delle risorse.

Se lavori in un ambiente multi-thread, assicurati di capire se il tuo oggetto è potenzialmente condiviso tra thread. Uno dei motivi principali per considerare l'utilizzo di boost :: shared_ptr o std :: tr1 :: shared_ptr è perché utilizza un conteggio dei riferimenti thread-safe.

Se sei preoccupato per l'allocazione separata dei conteggi di riferimento, ci sono molti modi per aggirare questo. Utilizzando la libreria boost :: shared_ptr è possibile raggruppare i contatori di riferimento o utilizzare boost :: make_shared (la mia preferenza) che alloca l'oggetto e il conteggio dei riferimenti in una singola allocazione, alleviando così la maggior parte dei problemi di cache cache che le persone hanno. È possibile evitare l'hit di prestazioni dell'aggiornamento del conteggio dei riferimenti nel codice critico delle prestazioni tenendo un riferimento all'oggetto al livello più alto e passando i riferimenti diretti all'oggetto.

Se hai bisogno di una proprietà condivisa ma non vuoi pagare il costo del conteggio dei riferimenti o della raccolta dei rifiuti, considera l'utilizzo di oggetti immutabili o una copia su idioma in scrittura.

Tieni presente che di gran lunga le tue più grandi vittorie in termini di prestazioni saranno a livello di architettura, seguite da un livello di algoritmo e, sebbene queste preoccupazioni di basso livello siano molto importanti, dovrebbero essere affrontate solo dopo aver affrontato i problemi principali. Se hai a che fare con problemi di prestazioni a livello di errori nella cache, hai tutta una serie di problemi che devi anche conoscere come la falsa condivisione che non ha nulla a che fare con i puntatori per dire.

Se stai utilizzando i puntatori intelligenti solo per condividere risorse come trame o modelli, prendi in considerazione una libreria più specializzata come Boost.Flyweight.

Una volta adottato il nuovo standard, spostare la semantica, i riferimenti ai valori e l'inoltro perfetto renderanno il lavoro con oggetti e contenitori costosi molto più semplice ed efficiente. Fino ad allora non archiviare i puntatori con semantica di copia distruttiva, come auto_ptr o unique_ptr, in un contenitore (il concetto standard). Prendi in considerazione l'utilizzo della libreria Boost.Pointer Container o l'archiviazione dei puntatori intelligenti di proprietà condivisa in Container. Nel codice critico per le prestazioni puoi considerare di evitarli entrambi a favore di contenitori intrusivi come quelli di Boost.Intrusive.

La piattaforma target non dovrebbe davvero influenzare troppo la tua decisione. Dispositivi integrati, smartphone, telefoni stupidi, PC e console possono eseguire correttamente il codice. Requisiti di progetto come budget di memoria rigidi o nessuna allocazione dinamica mai / dopo il caricamento sono preoccupazioni più valide e dovrebbero influenzare le tue scelte.


3
La gestione delle eccezioni sulle console può essere un po 'complicata - l'XDK in particolare è una specie di eccezione-ostile.
Crashworks,

1
La piattaforma target dovrebbe davvero influenzare il tuo design. L'hardware che trasforma i tuoi dati a volte può avere una grande influenza sul tuo codice sorgente. L'architettura PS3 è un esempio concreto in cui è davvero necessario coinvolgere l'hardware nella progettazione delle risorse, della gestione della memoria e del renderer.
Simon,

Non sono d'accordo solo leggermente, in particolare per quanto riguarda GC. Il più delle volte, i riferimenti ciclici non sono un problema per gli schemi contati di riferimento. Generalmente questi problemi ciclici di proprietà sorgono perché le persone non pensavano correttamente alla proprietà degli oggetti. Solo perché un oggetto deve puntare a qualcosa, non significa che dovrebbe possedere quel puntatore. L'esempio comunemente citato è puntatori posteriori negli alberi, ma il genitore del puntatore in un albero può tranquillamente essere un puntatore grezzo senza sacrificare la sicurezza.
Tim Seguine,

4

Se stai usando C ++ 0x, usa std::unique_ptr<T>.

Non ha un sovraccarico prestazionale, a differenza del std::shared_ptr<T>quale ha un sovraccarico di conteggio dei riferimenti. Un unique_ptr possiede il suo puntatore e puoi trasferire la proprietà con la semantica di spostamento di C ++ 0x . Non puoi copiarli - spostali solo.

Può anche essere utilizzato in contenitori, ad esempio std::vector<std::unique_ptr<T>>, che è binario compatibile e identico nelle prestazioni std::vector<T*>, ma non perderà memoria se si cancellano elementi o si cancella il vettore. Ciò ha anche una migliore compatibilità con gli algoritmi STL rispetto a ptr_vector.

IMO per molti scopi questo è un contenitore ideale: accesso casuale, eccezioni sicure, previene perdite di memoria, sovraccarico basso per la riallocazione vettoriale (si trascina solo attorno ai puntatori dietro le quinte). Molto utile per molti scopi.


3

È buona norma documentare quali classi possiedono i puntatori. Preferibilmente, usi semplicemente oggetti normali e nessun puntatore ogni volta che puoi.

Tuttavia, quando è necessario tenere traccia delle risorse, passare i puntatori è l'unica opzione. Ci sono alcuni casi:

  • Ottieni il puntatore da qualche altra parte, ma non lo gestisci: basta usare un puntatore normale e documentarlo in modo che nessun programmatore dopo aver provato a eliminarlo.
  • Ottieni il puntatore da qualche altra parte e ne tieni traccia: usa un scoped_ptr.
  • Ottieni il puntatore da qualche altra parte e tieni traccia di esso ma ha bisogno di un metodo speciale per eliminarlo: usa shared_ptr con un metodo di eliminazione personalizzato.
  • È necessario il puntatore in un contenitore STL: verrà copiato quindi sarà necessario boost :: shared_ptr.
  • Molte classi condividono il puntatore e non è chiaro chi lo eliminerà: shared_ptr (il caso sopra è in realtà un caso speciale di questo punto).
  • Crea tu stesso il puntatore e ne hai solo bisogno: se davvero non puoi usare un oggetto normale: scoped_ptr.
  • Crei il puntatore e lo condividerai con altre classi: shared_ptr.
  • Crei il puntatore e lo passi: usa un normale puntatore e documenta la tua interfaccia in modo che il nuovo proprietario sappia che dovrebbe gestire la risorsa da solo!

Penso che riguardi praticamente come gestisco le mie risorse in questo momento. Il costo di memoria di un puntatore come shared_ptr è generalmente il doppio del costo di memoria di un puntatore normale. Non penso che questo sovraccarico sia troppo grande, ma se hai poche risorse dovresti considerare di progettare il tuo gioco per ridurre il numero di puntatori intelligenti. In altri casi, ho appena progettato buoni principi come i proiettili sopra e il profiler mi dirà dove avrò bisogno di più velocità.


1

Per quanto riguarda in particolare i suggerimenti di boost, penso che dovrebbero essere evitati fintanto che la loro implementazione non è esattamente ciò di cui hai bisogno. Arrivano ad un costo che è più grande di quanto inizialmente qualcuno si aspetterebbe. Forniscono un'interfaccia che consente di saltare parti vitali e importanti della gestione della memoria e delle risorse.

Quando si tratta di qualsiasi sviluppo software, penso che sia importante pensare ai tuoi dati. È molto importante il modo in cui i tuoi dati sono rappresentati in memoria. La ragione di ciò è che la velocità della CPU è aumentata ad un ritmo molto maggiore rispetto al tempo di accesso alla memoria. Questo spesso rende la memoria cache il principale collo di bottiglia della maggior parte dei moderni giochi per computer. Avere i dati allineati linearmente nella memoria in base all'ordine di accesso è molto più intuitivo per la cache. Questo tipo di soluzioni spesso porta a progetti più puliti, codice più semplice e codice sicuramente più facile da eseguire il debug. I puntatori intelligenti portano facilmente a frequenti allocazioni dinamiche di memoria delle risorse, questo le fa sparpagliare in tutta la memoria.

Questa non è un'ottimizzazione prematura, è una decisione salutare che può e deve essere presa il prima possibile. È una questione di comprensione architettonica dell'hardware su cui verrà eseguito il software ed è importante.

Modifica: ci sono alcune cose da considerare riguardo alle prestazioni dei puntatori condivisi:

  • Il contatore di riferimento è allocato in heap.
  • Se si utilizza la sicurezza thread-enabled, il conteggio dei riferimenti viene eseguito tramite operazioni interbloccate.
  • Passare il puntatore in base al valore modifica il conteggio dei riferimenti, il che significa molto probabilmente operazioni interbloccate che utilizzano l'accesso casuale in memoria (blocchi + probabile cache miss).

2
Mi hai perso a "evitato a tutti i costi". Quindi prosegui descrivendo un tipo di ottimizzazione che raramente è un problema per i giochi del mondo reale. La maggior parte dello sviluppo di giochi è caratterizzata da problemi di sviluppo (ritardi, bug, giocabilità, ecc.) Non da una mancanza di prestazioni della cache della CPU. Quindi non sono assolutamente d'accordo con l'idea che questo consiglio non sia un'ottimizzazione prematura.
kevin42,

2
Sono d'accordo con la progettazione iniziale del layout dei dati. È importante ottenere qualsiasi prestazione da una moderna console / dispositivo mobile ed è qualcosa che non dovrebbe mai essere trascurato.
Olly,

1
Questo è un problema che ho visto in uno degli studi AAA in cui ho lavorato. Puoi anche ascoltare il capo architetto di Insomniac Games, Mike Acton. Non sto dicendo che boost sia una cattiva libreria, non è adatto solo per giochi ad alte prestazioni.
Simone,

1
@ kevin42: la coerenza della cache è probabilmente la principale fonte di ottimizzazioni di basso livello nello sviluppo del gioco oggi. @Simon: la maggior parte delle implementazioni shared_ptr evita i blocchi su qualsiasi piattaforma che supporti il ​​confronto e lo scambio, che include PC Linux e Windows, e credo che includa Xbox.

1
@Joe Wreschnig: è vero, la cache-miss è ancora molto probabilmente causando qualsiasi inizializzazione di un puntatore condiviso (copia, creazione da puntatore debole, ecc.). Una mancanza di cache L2 sui PC moderni è come 200 cicli e sul PPC (xbox360 / ps3) è maggiore. Con un gioco intenso potresti avere fino a 1000 oggetti di gioco, dato che ogni oggetto di gioco può avere parecchie risorse che stiamo esaminando problemi in cui il loro ridimensionamento è una delle maggiori preoccupazioni. Ciò probabilmente causerà problemi alla fine di un ciclo di sviluppo (quando colpirai l'elevata quantità di oggetti di gioco).
Simon,

0

Tendo a utilizzare i puntatori intelligenti ovunque. Non sono sicuro che sia una buona idea, ma sono pigro e non riesco a vedere alcun vero svantaggio [tranne se volevo fare un po 'di aritmetica del puntatore in stile C]. Uso boost :: shared_ptr perché so che posso copiarlo in giro - se due entità condividono un'immagine, quindi se una muore l'altra non dovrebbe perdere anche l'immagine.

Il rovescio della medaglia di questo è se un oggetto cancella qualcosa che punta e possiede, ma anche qualcos'altro lo punta, quindi non viene eliminato.


1
Ho usato share_ptr anche dappertutto, ma oggi provo a pensare se ho davvero bisogno della proprietà condivisa per alcuni dati. In caso contrario, potrebbe essere ragionevole rendere tali dati un membro non puntatore alla struttura di dati padre. Trovo che una chiara proprietà semplifichi i progetti.
jmp97,

0

I vantaggi della gestione della memoria e della documentazione forniti da buoni puntatori intelligenti significano che li uso regolarmente. Tuttavia, quando il profiler si dirige e mi dice che un utilizzo particolare mi sta costando, tornerò indietro a una gestione più puntuale del neolitico.


0

Sono vecchio, oldskool e un contatore di cicli. Nel mio lavoro uso puntatori non elaborati e nessuna allocazione dinamica in fase di esecuzione (tranne i pool stessi). Tutto è raggruppato e la proprietà è molto rigida e mai trasferibile, se davvero necessario scrivo un allocatore di piccoli blocchi personalizzato. Mi assicuro che ci sia uno stato durante il gioco per ogni pool per cancellare se stesso. Quando le cose diventano pelose, avvolgo gli oggetti nelle maniglie in modo da poterli spostare, ma preferirei di no. I contenitori sono ossa personalizzate ed estremamente nude. Inoltre non riutilizzo il codice.
Anche se non direi mai la virtù di tutti i puntatori intelligenti, i contenitori, gli iteratori e quant'altro, sono noto per essere in grado di programmare in modo estremamente veloce (e ragionevolmente affidabile, anche se non è consigliabile che gli altri saltino nel mio codice per ragioni piuttosto ovvie, come attacchi di cuore e incubi perpetui).

Al lavoro, ovviamente, tutto è diverso, a meno che non stia prototipando, cosa che per fortuna riesco a fare molto.


0

Quasi nessuno, anche se questa è certamente una risposta strana, e probabilmente non è vicino a nessuno adatto a tutti.

Ma ho trovato molto più utile nel mio caso personale memorizzare tutte le istanze di un particolare tipo in una sequenza centrale ad accesso casuale (thread-safe) e invece di lavorare con indici a 32 bit (indirizzi relativi, ad es.) , piuttosto che puntatori assoluti.

Per iniziare:

  1. Dimezza i requisiti di memoria del puntatore analogico su piattaforme a 64 bit. Finora non ho mai avuto bisogno di più di ~ 4,29 miliardi di istanze di un particolare tipo di dati.
  2. Fa in modo che tutte le istanze di un particolare tipo, Tnon saranno mai troppo disperse nella memoria. Ciò tende a ridurre i mancati cache per tutti i tipi di schemi di accesso, anche attraversando strutture collegate come alberi se i nodi sono collegati insieme usando indici anziché puntatori.
  3. I dati paralleli diventano facili da associare usando array paralleli economici (o array sparsi) invece di alberi o tabelle di hash.
  4. Impostare le intersezioni può essere trovato in tempo lineare o meglio usando, diciamo, un bitset parallelo.
  5. Possiamo radixizzare gli indici e ottenere un modello di accesso sequenziale molto adatto alla cache.
  6. Siamo in grado di tenere traccia di quante istanze è stato assegnato un determinato tipo di dati.
  7. Riduce al minimo il numero di posti che devono occuparsi di cose come la sicurezza delle eccezioni, se ti interessa quel genere di cose.

Detto questo, la convenienza è un aspetto negativo e la sicurezza del tipo. Non possiamo accedere a un'istanza di Tsenza avere accesso sia al contenitore che all'indice. E un vecchio int32_tnon ci dice nulla su quale tipo di dati si riferisca, quindi non c'è sicurezza del tipo. Potremmo accidentalmente provare ad accedere a Barusando un indice a Foo. Per mitigare il secondo problema, faccio spesso questo genere di cose:

struct FooIndex
{
    int32_t index;
};

Che sembra un po 'sciocco ma mi restituisce la sicurezza del tipo in modo che le persone non possano accidentalmente provare ad accedere a Barattraverso un indice Foosenza un errore del compilatore. Per comodità, accetto solo il leggero inconveniente.

Un'altra cosa che potrebbe essere un grosso inconveniente per le persone è che non posso usare il polimorfismo basato sull'ereditarietà in stile OOP, dal momento che ciò richiederebbe un puntatore di base che possa puntare a tutti i tipi di sottotipi diversi con dimensioni e requisiti di allineamento diversi. Ma in questi giorni non uso molto l'eredità: preferisco l'approccio ECS.

Per quanto riguarda shared_ptr, provo a non usarlo così tanto. Il più delle volte non trovo sensato condividere la proprietà e farlo in modo casuale può portare a perdite logiche. Spesso almeno ad alto livello, una cosa tende a appartenere a una cosa. Dove spesso trovavo allettante usare shared_ptrera prolungare la durata di un oggetto in luoghi che non si occupavano così tanto della proprietà, come solo una funzione locale in un thread per assicurarsi che l'oggetto non fosse distrutto prima che il thread fosse finito usandolo.

Per affrontare quel problema, invece di usare shared_ptro GC o qualcosa del genere, spesso preferisco attività di breve durata in esecuzione da un pool di thread e lo faccio in modo che se quel thread richiede di distruggere un oggetto, che la distruzione effettiva sia rinviata a una cassaforte momento in cui il sistema può garantire che nessun thread debba accedere a detto tipo di oggetto.

A volte finisco ancora con il conteggio dei ref, ma lo tratto come una strategia dell'ultima risorsa. E ci sono alcuni casi in cui ha davvero senso condividere la proprietà, come l'implementazione di una struttura di dati persistente, e lì trovo che abbia perfettamente senso cercare shared_ptrsubito.

Quindi, per lo più, uso principalmente gli indici e uso con parsimonia puntatori sia grezzi che intelligenti. Mi piacciono gli indici e il tipo di porte che si aprono quando sai che i tuoi oggetti sono archiviati in modo contiguo e non sparsi nello spazio della memoria.

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.