Quali sono gli usi del "posizionamento nuovo"?


411

Qualcuno qui ha mai usato il "posizionamento nuovo" di C ++? In tal caso, per cosa? Mi sembra che sarebbe utile solo su hardware mappato in memoria.


14
Queste sono solo le informazioni che stavo cercando, per chiamare i costruttori di oggetti su pool di memoria allocati boost. (Sperando che queste parole chiave rendano più facile per qualcuno trovare in futuro).
Sideshow Bob,

2
È usato nell'articolo di Wikipedia in C ++ 11 nel costruttore di un sindacato.
Ciao Arrivederci

@Ciao, arrivederci, interessante! Nell'articolo che hai collegato, perché non puoi semplicemente fare p = pte usare l'operatore di assegnazione Pointinvece di farlo new(&p) Point(pt)? Mi chiedo le differenze tra i due. Il primo chiamerebbe operator=Point, mentre il secondo chiama il costruttore di copia Point? ma non sono ancora molto chiaro perché uno sia migliore dell'altro.
Andrei-Niculae Petre,

@ Andrei-NiculaePetre Non ho usato il posizionamento nuovo me stesso, ma immagino che dovresti usarlo - insieme al costruttore della copia - se non hai attualmente un oggetto di quella classe, altrimenti dovresti usare l'operatore di assegnazione della copia. A meno che la classe non sia banale; quindi non importa quale di essi usi. La stessa cosa vale per la distruzione dell'oggetto. Non riuscire a gestirlo correttamente per le classi non banali può molto probabilmente portare a comportamenti strani e potrebbe persino causare comportamenti indefiniti in alcune situazioni.
Ciao Arrivederci

@ Andrei-NiculaePetre In realtà, trovo l' esempio nell'articolo di Wikipedia abbastanza male, dato che presume che non esista alcun oggetto precedente e che debbano costruirne uno. Questo non è il caso se U::operator=è appena stato chiamato.
Ciao Arrivederci

Risposte:


365

Il posizionamento nuovo consente di costruire un oggetto in memoria già allocato.

Potrebbe essere utile eseguire questa operazione per l'ottimizzazione quando è necessario costruire più istanze di un oggetto ed è più veloce non riassegnare la memoria ogni volta che è necessaria una nuova istanza. Al contrario, potrebbe essere più efficiente eseguire una singola allocazione per un pezzo di memoria che può contenere più oggetti, anche se non si desidera utilizzarlo tutto in una volta.

DevX offre un buon esempio :

Lo standard C ++ supporta anche il posizionamento di un nuovo operatore, che costruisce un oggetto su un buffer pre-allocato. Ciò è utile quando si crea un pool di memoria, un garbage collector o semplicemente quando le prestazioni e la sicurezza delle eccezioni sono fondamentali (non c'è pericolo di errore di allocazione poiché la memoria è già stata allocata e la costruzione di un oggetto su un buffer pre-allocato richiede meno tempo) :

char *buf  = new char[sizeof(string)]; // pre-allocated buffer
string *p = new (buf) string("hi");    // placement new
string *q = new string("hi");          // ordinary heap allocation

È inoltre possibile essere certi che non ci possano essere errori di allocazione in una determinata parte del codice critico (ad esempio, nel codice eseguito da un pacemaker). In tal caso, si desidera allocare memoria in precedenza, quindi utilizzare il posizionamento nuovo nella sezione critica.

Deallocation in position new

Non devi deallocare tutti gli oggetti che utilizzano il buffer di memoria. Dovresti invece eliminare [] solo il buffer originale. Dovresti quindi chiamare manualmente i distruttori delle tue classi. Per un buon suggerimento al riguardo, consulta le FAQ di Stroustrup su: Esiste un "eliminazione del posizionamento" ?


54
Non è obsoleto in quanto è necessaria questa funzione per implementare in modo efficiente oggetti contenitore (come il vettore). Se non si crea il proprio contenitore, non è necessario utilizzare questa funzione.
Martin York,

26
È anche molto importante ricordare di #include <memory>, altrimenti potresti incontrare alcuni terribili mal di testa su alcune piattaforme che non riconoscono automaticamente il posizionamento nuovo
Ramon Zarazua B.

22
In senso stretto, è un comportamento indefinito richiamare delete[]il charbuffer originale . L'uso del posizionamento newha terminato la durata degli charoggetti originali riutilizzando la loro memoria. Se ora chiami delete[] bufil tipo dinamico degli oggetti indicati non corrisponde più al loro tipo statico, quindi hai un comportamento indefinito. È più coerente utilizzare operator new/ operator deleteallocare memoria grezza destinata all'uso per posizionamento new.
CB Bailey,

31
Vorrei sicuramente saltare usando l'heap in un pacemaker :-)
Eli Bendersky

15
@RamonZarazua Intestazione sbagliata, lo è #include <new>.
bit2shift

63

Lo usiamo con pool di memoria personalizzati. Solo uno schizzo:

class Pool {
public:
    Pool() { /* implementation details irrelevant */ };
    virtual ~Pool() { /* ditto */ };

    virtual void *allocate(size_t);
    virtual void deallocate(void *);

    static Pool::misc_pool() { return misc_pool_p; /* global MiscPool for general use */ }
};

class ClusterPool : public Pool { /* ... */ };
class FastPool : public Pool { /* ... */ };
class MapPool : public Pool { /* ... */ };
class MiscPool : public Pool { /* ... */ };

// elsewhere...

void *pnew_new(size_t size)
{
   return Pool::misc_pool()->allocate(size);
}

void *pnew_new(size_t size, Pool *pool_p)
{
   if (!pool_p) {
      return Pool::misc_pool()->allocate(size);
   }
   else {
      return pool_p->allocate(size);
   }
}

void pnew_delete(void *p)
{
   Pool *hp = Pool::find_pool(p);
   // note: if p == 0, then Pool::find_pool(p) will return 0.
   if (hp) {
      hp->deallocate(p);
   }
}

// elsewhere...

class Obj {
public:
   // misc ctors, dtors, etc.

   // just a sampling of new/del operators
   void *operator new(size_t s)             { return pnew_new(s); }
   void *operator new(size_t s, Pool *hp)   { return pnew_new(s, hp); }
   void operator delete(void *dp)           { pnew_delete(dp); }
   void operator delete(void *dp, Pool*)    { pnew_delete(dp); }

   void *operator new[](size_t s)           { return pnew_new(s); }
   void *operator new[](size_t s, Pool* hp) { return pnew_new(s, hp); }
   void operator delete[](void *dp)         { pnew_delete(dp); }
   void operator delete[](void *dp, Pool*)  { pnew_delete(dp); }
};

// elsewhere...

ClusterPool *cp = new ClusterPool(arg1, arg2, ...);

Obj *new_obj = new (cp) Obj(arg_a, arg_b, ...);

Ora puoi raggruppare gli oggetti in una singola arena di memoria, selezionare un allocatore che è molto veloce ma senza deallocazione, utilizzare la mappatura della memoria e qualsiasi altro semantico che desideri imporre scegliendo il pool e passandolo come argomento al posizionamento di un oggetto nuovo operatore.


1
Sì. Ne siamo abbastanza intelligenti, ma è fuori tema per questa domanda.
Don Wakefield,

2
@jdkoftinoff hai qualche link a un esempio di codice reale? sembra piuttosto interessante per me!
Victor,

@DonWakefield Come gestisci l'allineamento in questo pool? Non dovresti passare l'allineamento come argomento da allocate()qualche parte?
Mikhail Vasilyev,

1
@MikhailVasilyev, in una vera implementazione, ovviamente lo gestiresti. Solo codice di esempio.
Don Wakefield,

cosa succede se il posizionamento è un indirizzo non valido, ad esempio 0x0?
Charlie,

51

È utile se si desidera separare l'allocazione dall'inizializzazione. STL utilizza il posizionamento nuovo per creare elementi contenitore.


35

L'ho usato nella programmazione in tempo reale. In genere non vogliamo eseguire alcuna allocazione dinamica (o deallocazione) dopo l'avvio del sistema, perché non esiste alcuna garanzia sul tempo necessario.

Quello che posso fare è preallocare un grosso pezzo di memoria (abbastanza grande da contenere qualsiasi quantità di ciò che la classe può richiedere). Quindi, una volta capito in fase di esecuzione come costruire le cose, il posizionamento nuovo può essere usato per costruire oggetti proprio dove li voglio. Una situazione in cui so di averlo usato era quella di aiutare a creare un buffer circolare eterogeneo .

Non è certamente per i deboli di cuore, ma è per questo che rendono la sintassi un po 'nodosa.


Ciao TED, potresti per favore condividere di più sulla soluzione che hai. Sto pensando a una soluzione pre-allocata ma non ho fatto molti progressi. Grazie in anticipo!
Viet

1
Bene, l'attuale vero e proprio codice di buffer circolare era davvero la parte difficile da ottenere. Il nuovo aspetto sembra un po 'orribile, ma in confronto non è stato affatto un problema.
TED

26

L'ho usato per costruire oggetti allocati nello stack tramite alloca ().

spudorato: ho bloggato su di esso qui .


articolo interessante, ma non sono sicuro di capire il vantaggio di usare questo sopra boost::array. Puoi espanderci un po '?
GrahamS,

boost :: array richiede che la dimensione dell'array sia una costante di tempo di compilazione. Questo non ha questa limitazione.
Ferruccio,

2
@Ferruccio Questo è abbastanza bello, ho notato che la tua macro è leggermente pericolosa, vale a dire che la dimensione potrebbe essere un'espressione. Se x + 1 viene passato, ad esempio, lo si espanderà in sizeof (tipo) * x + 1 che sarebbe errato. È necessario raggruppare la macro per renderla più sicura.
Benj

L'uso con alloca mi sembra pericoloso se viene generata un'eccezione poiché devi chiamare i distruttori su tutti i tuoi oggetti.
CashCow,

14

Head Geek: BINGO! Hai capito perfettamente: è esattamente per questo che è perfetto. In molti ambienti incorporati, i vincoli esterni e / o lo scenario d'uso generale costringono il programmatore a separare l'allocazione di un oggetto dalla sua inizializzazione. Insieme, C ++ chiama questa "istanza"; ma ogni volta che l'azione del costruttore deve essere esplicitamente invocata SENZA allocazione dinamica o automatica, il posizionamento nuovo è il modo per farlo. È anche il modo perfetto per individuare un oggetto C ++ globale bloccato all'indirizzo di un componente hardware (I / O mappato in memoria) o per qualsiasi oggetto statico che, per qualsiasi motivo, deve risiedere a un indirizzo fisso.


12

L'ho usato per creare una classe Variant (cioè un oggetto che può rappresentare un singolo valore che può essere uno di un numero di diversi tipi).

Se tutti i tipi di valore supportati dalla classe Variant sono tipi POD (ad esempio int, float, double, bool), è sufficiente un'unione in stile C con tag, ma se si desidera che alcuni dei tipi di valore siano oggetti C ++ ( ad es. std :: string), la funzione unione C non funzionerà, poiché i tipi di dati non POD potrebbero non essere dichiarati come parte di un'unione.

Quindi invece allocare un array di byte che è abbastanza grande (es. Sizeof (the_largest_data_type_I_support)) e utilizzo il posizionamento new per inizializzare l'oggetto C ++ appropriato in quell'area quando Variant è impostato per contenere un valore di quel tipo. (E il posizionamento viene eliminato in anticipo quando si passa da un diverso tipo di dati non POD, ovviamente)


Ehm, i tipi di dati non POD possono essere dichiarati all'interno di un'unione, purché si fornisca un ctor dell'unione - ed ehi - che probabilmente ilnew ctor userebbe il posizionamento per inizializzare la sua sottoclasse non POD. Rif: stackoverflow.com/a/33289972/2757035 Reinventare questa ruota usando un array di byte arbitrariamente grande è un pezzo impressionante di acrobazie ma sembra del tutto superfluo, quindi, cosa mi sono perso? :)
underscore_d

6
Hai perso tutte le versioni di C ++ precedenti a C ++ 11, che in molti casi devono ancora essere supportate. :)
Jeremy Friesner il

10

Il posizionamento nuovo è anche molto utile durante la serializzazione (diciamo con boost :: serializzazione). In 10 anni di c ++ questo è solo il secondo caso per cui ho avuto bisogno di un nuovo posizionamento (terzo se includi interviste :)).


9

È anche utile quando si desidera reinizializzare strutture globali o allocate staticamente.

Il vecchio modo C stava usando memset()per impostare tutti gli elementi su 0. Non è possibile farlo in C ++ a causa di vtables e costruttori di oggetti personalizzati.

Quindi a volte uso quanto segue

 static Mystruct m;

 for(...)  {
     // re-initialize the structure. Note the use of placement new
     // and the extra parenthesis after Mystruct to force initialization.
     new (&m) Mystruct();

     // do-some work that modifies m's content.
 }

1
Non avresti bisogno di fare una distruzione corrispondente prima di reinizializzarla in quel modo?
Head Geek

[Modificato per l'ortografia] Di solito - lo fai. Ma a volte, quando sai che la classe non alloca memoria o altre risorse (o le hai allocate esternamente, ad esempio quando usi pool di memoria), puoi usare questa tecnica. Garantisce che i puntatori v-table non vengano sovrascritti. - nimrodm 16 ore fa
nimrodm

1
Anche in C, l'uso di tutti i bit su 0 è garantito solo per produrre una rappresentazione di 0 per i tipi integrali, non altri tipi (il puntatore null può avere una rappresentazione diversa da zero).
curioso

@curiousguy - per i tipi primitivi hai ragione (renderà prevedibile il programma, il che è un vantaggio quando si tratta di debug). Tuttavia, i tipi di dati C ++ avranno il loro costruttore eseguito (sul posto) e saranno inizializzati correttamente.
nimrodm,

9

Penso che questo non sia stato evidenziato da nessuna risposta, ma un altro buon esempio e utilizzo per il nuovo posizionamento è quello di ridurre la frammentazione della memoria (usando pool di memoria). Ciò è particolarmente utile nei sistemi integrati e ad alta disponibilità. In quest'ultimo caso è particolarmente importante perché per un sistema che deve funzionare 24/365 giorni è molto importante non avere frammentazione. Questo problema non ha nulla a che fare con la perdita di memoria.

Anche quando viene utilizzata un'ottima implementazione di malloc (o simile funzione di gestione della memoria) è molto difficile gestire la frammentazione per molto tempo. Ad un certo punto se non gestisci in modo intelligente le chiamate di prenotazione / rilascio della memoria, potresti finire con un sacco di piccole lacune che sono difficili da riutilizzare (assegnare a nuove prenotazioni). Pertanto, una delle soluzioni utilizzate in questo caso è utilizzare un pool di memoria per allocare preventivamente la memoria per gli oggetti dell'applicazione. Dopodiché ogni volta che hai bisogno di memoria per qualche oggetto, devi semplicemente usare il nuovo posizionamento per creare un nuovo oggetto nella memoria già riservata.

In questo modo, una volta avviata l'applicazione, avrai già tutta la memoria necessaria riservata. Tutta la nuova prenotazione / versione della memoria va ai pool allocati (è possibile disporre di diversi pool, uno per ogni diversa classe di oggetti). In questo caso non si verifica alcuna frammentazione della memoria poiché non vi sono lacune e il sistema può funzionare per periodi molto lunghi (anni) senza soffrire di frammentazione.

L'ho visto in pratica appositamente per VxWorks RTOS poiché il suo sistema di allocazione di memoria predefinito soffre molto di frammentazione. Quindi l'allocazione di memoria attraverso il metodo new / malloc standard era sostanzialmente vietata nel progetto. Tutte le prenotazioni di memoria dovrebbero andare a un pool di memoria dedicato.


9

In realtà è necessario implementare qualsiasi tipo di struttura di dati che alloca più memoria di quella minima richiesta per il numero di elementi inseriti (ovvero qualsiasi cosa diversa da una struttura collegata che alloca un nodo alla volta).

Contenitori Prendere piace unordered_map, vectoro deque. Tutti questi allocano più memoria di quanto è minimamente necessario per gli elementi che hai inserito finora per evitare di richiedere un'allocazione di heap per ogni singolo inserimento. Usiamo vectorcome esempio più semplice.

Quando lo fai:

vector<Foo> vec;

// Allocate memory for a thousand Foos:
vec.reserve(1000);

... che in realtà non costruisce un migliaio di Foos. Alloca semplicemente / riserva la memoria per loro. Se vectornon utilizzassi il posizionamento nuovo qui, sarebbe una costruzione predefinita in Foostutto il luogo, oltre a dover invocare i loro distruttori anche per elementi che non hai mai nemmeno inserito in primo luogo.

Allocation! = Construction, Freeing! = Destruction

In generale, per implementare molte strutture di dati come sopra, non è possibile trattare l'allocazione della memoria e la costruzione di elementi come una cosa indivisibile, e allo stesso modo non è possibile trattare la liberazione della memoria e la distruzione di elementi come una cosa indivisibile.

Deve esserci una separazione tra queste idee per evitare costruttori e distruttori inutilmente invocati a destra e sinistra, ed è per questo che la libreria standard separa l'idea di std::allocator(che non costruisce o distrugge elementi quando alloca / libera memoria *) da i contenitori che lo usano che costruiscono manualmente gli elementi usando il posizionamento nuovo e distruggono manualmente gli elementi usando invocazioni esplicite dei distruttori.

  • Odio il design, std::allocatorma è un argomento diverso che eviterò di discutere. :-D

Quindi, comunque, tendo ad usarlo molto poiché ho scritto un certo numero di contenitori C ++ conformi agli standard per scopi generici che non potevano essere costruiti in termini di quelli esistenti. Tra questi c'è una piccola implementazione vettoriale che ho costruito un paio di decenni fa per evitare allocazioni di heap nei casi più comuni e un trie efficiente in termini di memoria (che non alloca un nodo alla volta). In entrambi i casi non sono stato in grado di implementarli utilizzando i contenitori esistenti, quindi ho dovuto utilizzare placement newper evitare costruttori e distruttori superflui su cose inutili a destra e a sinistra.

Naturalmente, se mai lavori con allocatori personalizzati per allocare oggetti singolarmente, come un elenco gratuito, allora in genere vorresti anche usarli placement new, come questo (esempio di base che non si preoccupa della sicurezza delle eccezioni o RAII):

Foo* foo = new(free_list.allocate()) Foo(...);
...
foo->~Foo();
free_list.free(foo);

8

È utile se stai costruendo un kernel - dove collochi il codice del kernel che leggi dal disco o la pagetable? Devi sapere dove saltare.

O in altre circostanze molto rare come quando hai un sacco di spazio assegnato e vuoi posizionare alcune strutture una dietro l'altra. Possono essere impacchettati in questo modo senza la necessità dell'operatore offsetof (). Ci sono altri trucchi anche per quello.

Credo anche che alcune implementazioni STL facciano uso di un posizionamento nuovo, come std :: vector. Allocare spazio per 2 ^ n elementi in questo modo e non è necessario riallocare sempre.


Ridurre le allocazioni di memoria è uno dei motivi principali per usarlo, così come i "trucchi" come il caricamento di oggetti dal disco
lefticus,

Non conosco nessun kernel scritto in C ++; la maggior parte dei kernel sono scritti in C.
Adam Rosenfield il

8
Il sistema operativo con cui ho imparato le basi del sistema operativo è scritto in C ++: sweb.sourceforge.net
mstrobl,

8

È usato std::vector<>perché in std::vector<>genere alloca più memoria di quella presente objectsnel file vector<>.


7

L'ho usato per archiviare oggetti con file mappati in memoria.
L'esempio specifico era un database di immagini che elaborava un gran numero di immagini di grandi dimensioni (più di quanto potesse contenere in memoria).


7

L'ho visto usato come una leggera modifica delle prestazioni per un puntatore di "tipo dinamico" (nella sezione "Roba da smanettoni"):

Ma ecco il trucco che ho usato per ottenere prestazioni veloci per piccoli tipi: se il valore trattenuto può adattarsi all'interno di un vuoto *, in realtà non mi preoccupo di allocare un nuovo oggetto, lo forzo nel puntatore stesso usando il posizionamento nuovo .


Cosa significa se il valore trattenuto può adattarsi all'interno di un vuoto * ? È sempre possibile assegnare qualsiasi tipo di puntatore a void *. Puoi per favore mostrarci qualche esempio?
anurag86,

@ anurag86: sulla mia macchina a 64 bit, a void*richiede 8 byte. È un po 'sciocco puntare un otto byte void*a un byte bool. Ma è del tutto possibile sovrapporre effettivamente l' boolon void*, proprio come a union { bool b; void* v }. Hai bisogno di un modo per sapere che la cosa che hai chiamato a void*è in realtà un bool(o un short, o un float, ecc.). L'articolo a cui ho collegato descrive come farlo. E, per rispondere alla domanda originale, il posizionamento newè la funzione utilizzata per creare un bool(o altro tipo) in cui void*è previsto un (i cast vengono utilizzati per ottenere / modificare in seguito il valore).
Max Lybbert,

@ anurag86: Non è la stessa cosa, ma potresti essere interessato ai puntatori con tag ( en.wikipedia.org/wiki/Tagged_pointer ).
Max Lybbert,

6

L'ho usato per creare oggetti basati sulla memoria contenente messaggi ricevuti dalla rete.


5

In genere, il posizionamento nuovo viene utilizzato per eliminare il costo di allocazione di un "nuovo normale".

Un altro scenario in cui l'ho usato è un posto in cui volevo avere accesso al puntatore a un oggetto che doveva ancora essere costruito, per implementare un singleton per documento.



4

L'unico posto in cui l'ho incontrato è in contenitori che allocano un buffer contiguo e quindi lo riempiono di oggetti come richiesto. Come accennato, std :: vector potrebbe farlo, e so che alcune versioni di MFC CArray e / o CList lo hanno fatto (perché è lì che l'ho incontrato per la prima volta). Il metodo di sovrallocazione del buffer è un'ottimizzazione molto utile e il posizionamento nuovo è praticamente l'unico modo per costruire oggetti in quello scenario. A volte viene anche usato per costruire oggetti in blocchi di memoria allocati al di fuori del codice diretto.

L'ho usato con una capacità simile, anche se non si presenta spesso. È uno strumento utile per il toolbox C ++, però.


4

I motori di script possono utilizzarlo nell'interfaccia nativa per allocare oggetti nativi dagli script. Vedi Angelscript (www.angelcode.com/angelscript) per esempi.


3

Vedi il file fp.h nel progetto xll su http://xll.codeplex.com Risolve il problema "ingiustizia ingiustificata con il compilatore" per le matrici che amano portare con sé le loro dimensioni.

typedef struct _FP
{
    unsigned short int rows;
    unsigned short int columns;
    double array[1];        /* Actually, array[rows][columns] */
} FP;

2

Ecco l'uso killer per il costruttore sul posto C ++: allineamento a una riga della cache, così come altre potenze di 2 confini. Ecco il mio algoritmo di allineamento del puntatore ultraveloce a qualsiasi potenza di 2 confini con 5 o meno istruzioni a ciclo singolo :

/* Quickly aligns the given pointer to a power of two boundary IN BYTES.
@return An aligned pointer of typename T.
@brief Algorithm is a 2's compliment trick that works by masking off
the desired number in 2's compliment and adding them to the
pointer.
@param pointer The pointer to align.
@param boundary_byte_count The boundary byte count that must be an even
power of 2.
@warning Function does not check if the boundary is a power of 2! */
template <typename T = char>
inline T* AlignUp(void* pointer, uintptr_t boundary_byte_count) {
  uintptr_t value = reinterpret_cast<uintptr_t>(pointer);
  value += (((~value) + 1) & (boundary_byte_count - 1));
  return reinterpret_cast<T*>(value);
}

struct Foo { Foo () {} };
char buffer[sizeof (Foo) + 64];
Foo* foo = new (AlignUp<Foo> (buffer, 64)) Foo ();

Ora non ti basta mettere un sorriso in faccia (:-). I ♥♥♥ C ++ 1x

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.