webentwicklung-frage-antwort-db.com.de

Welche Verwendungen gibt es für "Placement New"?

Hat jemand hier jemals C++ 's "Placement New" benutzt? Wenn ja, wofür? Es sieht für mich so aus, als wäre es nur für speicherabgebildete Hardware nützlich.

375
Head Geek

Mit der Option "Neu platzieren" können Sie ein Objekt im Speicher erstellen, das bereits zugeordnet ist.

Möglicherweise möchten Sie dies zur Optimierung tun, wenn Sie mehrere Instanzen eines Objekts erstellen müssen und es schneller ist, den Speicher nicht jedes Mal neu zuzuweisen, wenn Sie eine neue Instanz benötigen. Stattdessen ist es möglicherweise effizienter, eine einzelne Zuordnung für einen Speicherbereich vorzunehmen, der mehrere Objekte enthalten kann, obwohl Sie nicht alle Objekte gleichzeitig verwenden möchten.

DevX gibt ein gutes Beispiel :

Standard C++ unterstützt auch das Platzieren eines neuen Operators, der ein Objekt in einem vorab zugewiesenen Puffer erstellt. Dies ist nützlich, wenn ein Speicherpool, ein Garbage Collector oder einfach nur Leistung und Ausnahmesicherheit im Vordergrund stehen (da der Speicher bereits zugewiesen wurde und das Erstellen eines Objekts in einem vorab zugewiesenen Puffer weniger Zeit in Anspruch nimmt, besteht keine Gefahr eines Zuweisungsfehlers). :

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

Möglicherweise möchten Sie auch sicherstellen, dass an einem bestimmten Teil des kritischen Codes kein Zuordnungsfehler auftritt (z. B. in Code, der von einem Schrittmacher ausgeführt wird). In diesem Fall sollten Sie den Speicher früher zuweisen und dann die Platzierung new innerhalb des kritischen Abschnitts verwenden.

Ausgliederung in Platzierung neu

Sie sollten nicht jedes Objekt freigeben, das den Speicherpuffer verwendet. Stattdessen sollten Sie [] nur den ursprünglichen Puffer löschen. Sie müssten dann die Destruktoren Ihrer Klassen manuell aufrufen. Einen guten Vorschlag dazu finden Sie in Stroustrup's FAQ on: Gibt es ein "Placement Delete" ?

338
Brian R. Bondy

Wir verwenden es mit benutzerdefinierten Speicherpools. Nur eine Skizze:

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, ...);

Jetzt können Sie Objekte in einem einzigen Speicherbereich zusammenfassen, einen Allokator auswählen, der sehr schnell ist, aber keine Freigabe vornimmt, die Speicherzuordnung verwenden und jede andere Semantik verwenden, die Sie auferlegen möchten, indem Sie den Pool auswählen und als Argument an die Platzierung eines Objekts übergeben neuer Betreiber.

57
Don Wakefield

Dies ist nützlich, wenn Sie die Zuordnung von der Initialisierung trennen möchten. STL verwendet die Platzierung new, um Containerelemente zu erstellen.

50
MSN

Ich habe es in Echtzeit-Programmierung verwendet. Normalerweise möchten nach dem Systemstart keine dynamische Zuordnung (oder Freigabe) vornehmen, da keine Garantie dafür besteht, wie lange dies dauern wird .

Was ich tun kann, ist, einen großen Teil des Speichers vorab zuzuweisen (groß genug, um eine beliebige Menge von allem aufzunehmen, was die Klasse benötigt). Wenn ich dann zur Laufzeit herausgefunden habe, wie die Dinge zu konstruieren sind, kann die Platzierung new verwendet werden, um Objekte genau dort zu konstruieren, wo ich sie haben möchte. Eine Situation, von der ich weiß, dass ich sie verwendet habe, bestand darin, einen heterogenen zirkulären Puffer zu erstellen.

Es ist sicherlich nichts für schwache Nerven, aber deswegen machen sie die Syntax dafür irgendwie knorrig.

32
T.E.D.

Ich habe es verwendet, um Objekte zu konstruieren, die über alloca () auf dem Stapel zugewiesen wurden.

Schamloser Plug: Ich habe darüber gebloggt hier .

23
Ferruccio

Head Geek: BINGO! Du hast es total verstanden - genau dafür ist es perfekt. In vielen eingebetteten Umgebungen zwingen externe Einschränkungen und/oder das allgemeine Verwendungsszenario den Programmierer, die Zuordnung eines Objekts von seiner Initialisierung zu trennen. Zusammengefasst nennt C++ diese "Instanziierung"; Wenn die Aktion des Konstruktors jedoch OHNE dynamische oder automatische Zuweisung explizit aufgerufen werden muss, können Sie sie durch Platzieren neu ausführen. Dies ist auch der perfekte Weg, um ein globales C++ - Objekt zu lokalisieren, das an die Adresse einer Hardwarekomponente (speicherabgebildete E/A) gebunden ist, oder für ein statisches Objekt, das sich aus irgendeinem Grund an einer festen Adresse befinden muss.

13
dorcsssc

Ich habe es verwendet, um eine Variant-Klasse zu erstellen (d. H. Ein Objekt, das einen einzelnen Wert darstellen kann, der einer von mehreren verschiedenen Typen angehören kann).

Wenn alle von der Variant-Klasse unterstützten Werttypen POD-Typen sind (z. B. int, float, double, bool), reicht eine Union im C-Stil mit Tags aus. Wenn Sie jedoch möchten, dass einige der Werttypen C++ -Objekte sind ( Beispiel: std :: string), die C-Union-Funktion funktioniert nicht, da Nicht-POD-Datentypen möglicherweise nicht als Teil einer Union deklariert werden.

Also ordne ich stattdessen ein Byte-Array zu, das groß genug ist (z. B. sizeof (the_largest_data_type_I_support)), und initialisiere das entsprechende C++ - Objekt in diesem Bereich mithilfe von placement new, wenn Variant so eingestellt ist, dass es einen Wert dieses Typs enthält. (Und das Löschen der Platzierung im Voraus, wenn Sie von einem anderen Nicht-POD-Datentyp wechseln.)

11
Jeremy Friesner

Dies ist auch nützlich, wenn Sie globale oder statisch zugewiesene Strukturen neu initialisieren möchten.

Die alte C-Methode verwendete memset(), um alle Elemente auf 0 zu setzen. Dies ist in C++ aufgrund von vtables und benutzerdefinierten Objektkonstruktoren nicht möglich.

Also benutze ich manchmal folgendes

 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.
 }
9
nimrodm

Die Neuplatzierung ist auch sehr nützlich bei der Serialisierung (z. B. mit boost :: serialization). In 10 Jahren von c ++ ist dies nur der zweite Fall, für den ich eine neue Platzierung benötigt habe (der dritte, wenn Sie Interviews einschließen :)).

8
RichardBruce

Es ist nützlich, wenn Sie einen Kernel erstellen. Wo platzieren Sie den Kernel-Code, den Sie von der Festplatte oder der Seitentabelle gelesen haben? Sie müssen wissen, wohin Sie springen müssen.

Oder in anderen, sehr seltenen Fällen, wenn Sie viel Platz zur Verfügung haben und ein paar Strukturen hintereinander platzieren möchten. Sie können auf diese Weise gepackt werden, ohne dass der offsetof () -Operator erforderlich ist. Es gibt aber auch andere Tricks dafür.

Ich glaube auch, dass einige STL-Implementierungen die Platzierung neu nutzen, wie std :: vector. Sie weisen auf diese Weise Platz für 2 ^ n Elemente zu und müssen nicht immer neu zuweisen.

8
mstrobl

Tatsächlich ist es erforderlich, jede Art von Datenstruktur zu implementieren, die mehr Speicher als minimal für die Anzahl der eingefügten Elemente benötigt (d. H. Alles andere als eine verknüpfte Struktur, die jeweils einen Knoten zuweist).

Nehmen Sie Container wie unordered_map, vector oder deque. Diese alle belegen mehr Speicher als für die bisher eingefügten Elemente minimal erforderlich ist, um zu vermeiden, dass für jede einzelne Einfügung eine Heap-Zuordnung erforderlich ist. Verwenden wir als einfachstes Beispiel vector.

Wenn Sie das tun:

vector<Foo> vec;

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

... das baut eigentlich nicht tausend Foos. Es reserviert einfach Speicher für sie. Wenn vector hier keine neue Platzierung verwendet, wird Foos standardmäßig überall erstellt und müssen ihre Destruktoren auch für Elemente aufgerufen werden, die Sie noch nie zuvor eingefügt haben.

Zuweisung! = Aufbau, Befreiung! = Zerstörung

Um viele Datenstrukturen wie die oben genannten zu implementieren, können Sie die Zuweisung von Speicher und die Erstellung von Elementen nicht als eine unteilbare Sache behandeln. Ebenso können Sie die Freigabe von Speicher und die Zerstörung von Elementen nicht als eine unteilbare Sache behandeln.

Es muss eine Trennung zwischen diesen Ideen geben, um zu vermeiden, dass Konstruktoren und Destruktoren unnötigerweise unnötig links und rechts aufgerufen werden, und deshalb trennt die Standardbibliothek die Idee von std::allocator (Die keine Elemente konstruiert oder zerstört, wenn sie allokiert/Gibt Speicher *) von den Containern frei, die Elemente manuell erstellen, indem sie neu platziert werden, und zerstört Elemente manuell, indem explizite Destruktoraufrufe ausgeführt werden.

  • Ich hasse das Design von std::allocator, Aber das ist ein anderes Thema, über das ich nicht schimpfen möchte. :-D

Trotzdem benutze ich es häufig, da ich eine Reihe von standardkonformen Allzweck-C++ - Containern geschrieben habe, die in Bezug auf die vorhandenen nicht erstellt werden konnten. Darunter befindet sich eine kleine Vektorimplementierung, die ich vor einigen Jahrzehnten erstellt habe, um häufige Heap-Zuweisungen zu vermeiden, sowie ein speichereffizienter Versuch (der nicht jeweils einen Knoten zuweist). In beiden Fällen konnte ich sie nicht wirklich mit den vorhandenen Containern implementieren, und so musste ich placement new Verwenden, um zu vermeiden, dass Konstruktoren und Destruktoren unnötig für Dinge aufgerufen werden, die links und rechts unnötig sind.

Wenn Sie jemals mit benutzerdefinierten Zuordnern arbeiten, um Objekte einzeln zuzuordnen, wie z. B. einer freien Liste, dann möchten Sie im Allgemeinen auch placement new Wie folgt verwenden (einfaches Beispiel, das sich nicht um Ausnahmesicherheit oder RAII kümmert ):

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

Ich denke, dass dies durch keine Antwort hervorgehoben wurde, aber ein weiteres gutes Beispiel und eine Verwendung für die neue Platzierung ist die Reduzierung der Speicherfragmentierung (durch Verwendung von Speicherpools) ). Dies ist besonders nützlich in eingebetteten Systemen und Systemen mit hoher Verfügbarkeit. In diesem letzten Fall ist dies besonders wichtig, da es für ein System, das 24/365 Tage laufen muss, sehr wichtig ist, keine Fragmentierung zu haben. Dieses Problem hat nichts mit Speicherverlust zu tun.

Selbst wenn eine sehr gute Malloc-Implementierung (oder eine ähnliche Speicherverwaltungsfunktion) verwendet wird, ist es sehr schwierig, lange Zeit mit Fragmentierung umzugehen. Wenn Sie die Speicherreservierungs-/Freigabeaufrufe nicht geschickt verwalten, kann es zu vielen kleinen Lücken kommen, die nur schwer wiederverwendbar sind (neuen Reservierungen zuweisen). Eine der in diesem Fall verwendeten Lösungen besteht darin, einen Speicherpool zu verwenden, um den Speicher für die Anwendungsobjekte vorab zuzuweisen. Danach können Sie jedes Mal, wenn Sie Speicher für ein Objekt benötigen, mit der neuen Platzierung ein neues Objekt auf dem bereits reservierten Speicher erstellen.

Auf diese Weise haben Sie nach dem Start Ihrer Anwendung bereits den gesamten benötigten Speicherplatz reserviert. Die gesamte neue Speicherreservierung/-freigabe geht an die zugewiesenen Pools (Sie können mehrere Pools haben, einen für jede unterschiedliche Objektklasse). In diesem Fall tritt keine Speicherfragmentierung auf, da keine Lücken entstehen und Ihr System sehr lange (Jahre) laufen kann, ohne fragmentiert zu werden.

Ich habe dies in der Praxis speziell für VxWorks RTOS gesehen, da sein Standard-Speicherzuweisungssystem stark fragmentiert ist. Das Zuweisen von Speicher nach der Standardmethode new/malloc war im Projekt also grundsätzlich verboten. Alle Speicherreservierungen sollten in einen dedizierten Speicherpool verschoben werden.

8
rkachach

Ich habe es zum Speichern von Objekten mit Speicherabbildern verwendet.
Das spezielle Beispiel war eine Bilddatenbank, die sehr viele große Bilder verarbeitete (mehr als in den Speicher passen konnten).

7
Martin Beckett

Es wird von std::vector<> da std::vector<> reserviert normalerweise mehr Speicher als objects im vector<>.

7

Ich habe es als leichten Performance-Hack für einen "Dynamic Type" -Pointer gesehen (im Abschnitt "Under the Hood"):

Aber hier ist der knifflige Trick, den ich verwendet habe, um für kleine Typen eine schnelle Leistung zu erzielen: Wenn der gehaltene Wert in eine Lücke passen kann *, muss ich eigentlich kein neues Objekt zuweisen, sondern erzwinge es in den Zeiger selbst, indem ich die Platzierung new verwende .

7
Max Lybbert

Ich habe es verwendet, um Objekte basierend auf dem Speicher zu erstellen, der vom Netzwerk empfangene Nachrichten enthält.

6
Steve Fallows

Im Allgemeinen wird die Platzierung neu verwendet, um die Zuordnungskosten eines "normalen Neu" zu beseitigen.

Ein anderes Szenario, in dem ich es verwendet habe, ist ein Ort, an dem ich Zugriff auf Zeiger auf ein noch zu erstellendes Objekt haben möchte, um einen dokumentenspezifischen Singleton zu implementieren.

5
xtofl
5
kralyk

Skriptmodule können es in der nativen Schnittstelle verwenden, um native Objekte aus Skripten zuzuweisen. Beispiele finden Sie unter Angelscript (www.angelcode.com/angelscript).

4
Raindog

Die einzige Stelle, auf die ich gestoßen bin, sind Container, die einen zusammenhängenden Puffer zuweisen und ihn dann nach Bedarf mit Objekten füllen. Wie bereits erwähnt, könnte std :: vector dies tun, und ich weiß, dass einige Versionen von MFC CArray und/oder CList dies getan haben (da ich dort zum ersten Mal darauf gestoßen bin). Die Pufferüberzuweisungsmethode ist eine sehr nützliche Optimierung, und das Platzieren neuer Objekte ist praktisch die einzige Möglichkeit, in diesem Szenario Objekte zu erstellen. Es wird manchmal auch verwendet, um Objekte in Speicherblöcken zu erstellen, die außerhalb Ihres direkten Codes zugeordnet sind.

Ich habe es in ähnlicher Weise benutzt, obwohl es nicht oft auftaucht. Es ist jedoch ein nützliches Tool für die C++ - Toolbox.

4
Nick

Die Datei fp.h im xll-Projekt finden Sie unter http://xll.codeplex.com . Sie behebt das Problem, dass Arrays ihre Dimensionen gerne mit sich herumtragen.

 typedef struct _FP 
 {
 vorzeichenlose kurze int-Zeilen; 
 vorzeichenlose kurze int-Spalten; 
 double array [1];/* Array [Zeilen] [Spalten] */
} FP; 
3
Keith A. Lewis

Hier ist die beste Verwendung für den C++ - In-Place-Konstruktor: Ausrichtung auf eine Cache-Zeile sowie andere Potenzen von 2 Grenzen. Hier ist mein ultraschneller Zeigerausrichtungsalgorithmus für jede Potenz von 2 Grenzen mit 5 oder weniger Einzelzyklusanweisungen :

/* 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 ();

Jetzt zaubert das nicht einfach ein Lächeln auf dein Gesicht (:-). Ich ♥♥♥ C++ 1x

2
user2356685