web-dev-qa-db-fra.com

Quels sont les usages pour "placement nouveau"?

Quelqu'un at-il déjà utilisé le "placement new" de C++? Si oui, pour quoi faire? Il me semble que cela ne serait utile que sur du matériel mappé en mémoire.

375
Head Geek

Le placement nouveau vous permet de construire un objet en mémoire déjà alloué.

Cela peut être utile pour l'optimisation lorsque vous devez créer plusieurs instances d'un objet et qu'il est plus rapide de ne pas réallouer de la mémoire chaque fois que vous avez besoin d'une nouvelle instance. Au lieu de cela, il serait peut-être plus efficace de procéder à une seule allocation pour un bloc de mémoire pouvant contenir plusieurs objets, même si vous ne souhaitez pas tout utiliser en même temps.

DevX donne un bon exemple :

Standard C++ prend également en charge le nouvel opérateur de placement, qui construit un objet sur un tampon préalloué. Ceci est utile lors de la construction d'un pool de mémoire, d'un ramasse-miettes ou tout simplement lorsque la sécurité des performances et des exceptions est primordiale (il n'y a aucun risque d'échec d'allocation puisque la mémoire a déjà été allouée, et la construction d'un objet sur un tampon pré-alloué prend moins de temps). :

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

Vous pouvez également vous assurer qu'il ne peut y avoir d'échec d'allocation à une certaine partie du code critique (par exemple, dans du code exécuté par un stimulateur cardiaque). Dans ce cas, vous voudriez allouer de la mémoire plus tôt, puis utiliser le placement nouveau dans la section critique.

Deallocation in placement new

Vous ne devez pas libérer tous les objets utilisant le tampon de mémoire. Au lieu de cela, vous devez supprimer [] uniquement le tampon d'origine. Vous devez ensuite appeler les destructeurs de vos classes manuellement. Pour une bonne suggestion à ce sujet, veuillez vous reporter à la FAQ de Stroustrup _ sur: Existe-t-il une "suppression de placement" ?

338
Brian R. Bondy

Nous l'utilisons avec des pools de mémoire personnalisés. Juste un croquis:

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

Vous pouvez maintenant regrouper des objets dans une seule arène de mémoire, sélectionner un allocateur très rapide mais ne faisant pas de désallocation, utiliser un mappage de mémoire et toute autre sémantique que vous souhaitez imposer en choisissant le pool et en le passant comme argument du placement d'un objet. nouvel opérateur.

57
Don Wakefield

C'est utile si vous voulez séparer l'allocation de l'initialisation. STL utilise placement new pour créer des éléments de conteneur.

50
MSN

Je l'ai utilisé dans la programmation en temps réel. Généralement, nous ne souhaitons pas effectuer une allocation dynamique (ou une désallocation) après le démarrage du système, car rien ne garantit combien de temps cela va prendre. .

Ce que je peux faire, c'est préallouer une grande quantité de mémoire (assez grande pour contenir tout ce dont la classe peut avoir besoin). Ensuite, une fois que j’ai compris au moment de l’exécution comment construire les choses, le placement nouveau peut être utilisé pour construire des objets là où je les veux. Une situation dans laquelle je sais que je l’ai utilisé consistait à créer un hétérogène tampon circulaire .

Ce n’est certainement pas pour les âmes sensibles, mais c’est la raison pour laquelle leur syntaxe est assez grossière.

32
T.E.D.

Je l'ai utilisé pour construire des objets alloués sur la pile via alloca ().

plug sans vergogne: J'ai blogué à ce sujet ici .

23
Ferruccio

Chef Geek: BINGO! Vous avez tout à fait - c'est exactement ce que c'est parfait pour. Dans de nombreux environnements intégrés, des contraintes externes et/ou le scénario d'utilisation globale oblige le programmeur à séparer l'allocation d'un objet de son initialisation. Cumpot ensemble, C++ appelle cela "instanciation"; mais chaque fois que l'action du constructeur doit être explicitement appelée SANS allocation dynamique ou automatique, le placement nouveau est le moyen de le faire. C'est également le moyen idéal pour localiser un objet global C++ épinglé à l'adresse d'un composant matériel (E/S mappée en mémoire) ou pour tout objet statique qui, pour une raison quelconque, doit résider à une adresse fixe.

13
dorcsssc

Je l’ai utilisé pour créer une classe Variant (c’est-à-dire un objet pouvant représenter une valeur unique pouvant être l’un des nombreux types).

Si tous les types de valeur pris en charge par la classe Variant sont des types POD (par exemple, int, float, double, bool), une union de style C étiquetée est suffisante, mais si vous souhaitez que certains types de valeur soient des objets C++ ( Par exemple, std :: string), la fonction union C ne suffira pas, car les types de données autres que POD ne peuvent pas être déclarés comme faisant partie d'une union.

Donc, au lieu de cela, j'alloue un tableau d'octets suffisamment grand (par exemple, sizeof (the_largest_data_type_I_support)) et utilise placement new pour initialiser l'objet C++ approprié dans cette zone lorsque le Variant est défini pour contenir une valeur de ce type. (Et bien sûr, vous supprimez préalablement l'emplacement lorsque vous vous éloignez d'un autre type de données non-POD)

11
Jeremy Friesner

C'est également utile lorsque vous souhaitez réinitialiser des structures globales ou allouées statiquement.

L'ancien mode C utilisait memset() pour définir tous les éléments sur 0. Vous ne pouvez pas le faire en C++ en raison de vtables et des constructeurs d'objet personnalisé.

Donc j'utilise parfois le suivant

 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

Le placement new est également très utile lors de la sérialisation (par exemple avec boost :: serialization). En 10 ans de c ++, il ne s'agit que du deuxième cas pour lequel j'ai besoin d'un nouveau placement (troisième si vous incluez des entrevues :)).

8
RichardBruce

Cela est utile si vous construisez un noyau - où placez-vous le code du noyau que vous lisez sur le disque ou la feuille de page? Vous devez savoir où aller.

Ou dans d'autres circonstances très rares, par exemple lorsque vous avez beaucoup de place et que vous souhaitez placer quelques structures l'une derrière l'autre. Ils peuvent être compressés de cette façon sans avoir besoin de l'opérateur offsetof (). Il y a d'autres astuces pour cela aussi, cependant.

Je pense aussi que certaines implémentations STL utilisent de nouveaux placements, comme std :: vector. Ils allouent ainsi de la place pour 2 ^ n éléments et n'ont pas besoin de toujours réallouer.

8
mstrobl

En fait, il est en quelque sorte nécessaire d'implémenter tout type de structure de données qui alloue plus de mémoire que le minimum requis pour le nombre d'éléments insérés (c'est-à-dire autre chose qu'une structure liée qui alloue un nœud à la fois).

Prenez des conteneurs tels que unordered_map, vector ou deque. Tous ces éléments allouent plus de mémoire que le minimum requis pour les éléments que vous avez insérés jusqu'à présent afin d'éviter de nécessiter une allocation de tas pour chaque insertion. Utilisons vector comme exemple le plus simple.

Quand tu fais:

vector<Foo> vec;

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

... cela ne construit pas réellement mille Foos. Il leur alloue/réserve simplement de la mémoire. Si vector n'utilisait pas le placement new ici, il s'agirait de construire par défaut Foos et de devoir invoquer leurs destructeurs même pour des éléments que vous n'avez jamais insérés.

Allocation! = Construction, Libération! = Destruction

De manière générale, pour implémenter de nombreuses structures de données comme celle ci-dessus, vous ne pouvez pas traiter l'allocation de mémoire et la construction d'éléments comme une seule chose indivisible, et vous ne pouvez pas non plus traiter la libération de mémoire et la destruction d'éléments comme une seule chose indivisible.

Il faut séparer ces idées pour éviter d'invoquer inutilement des constructeurs et des destructeurs inutilement gauche et droite. C'est pourquoi la bibliothèque standard sépare l'idée de std::allocator (qui ne construit ni ne détruit d'éléments lorsqu'il alloue/libère memory *) loin des conteneurs qui l'utilisent qui construisent manuellement des éléments en utilisant le placement et les détruisent manuellement à l'aide d'appels explicites de destructeurs.

  • Je déteste le design de std::allocator mais c’est un sujet différent sur lequel je ne parlerai pas. :-RÉ

En tout état de cause, j’ai tendance à l’utiliser beaucoup, car j’ai écrit un certain nombre de conteneurs C++ universels conformes à la norme qui n’ont pas pu être construits à l’aide des conteneurs existants. Parmi eux, on trouve une petite implémentation vectorielle que j'ai construite il y a quelques décennies pour éviter les allocations de tas dans les cas courants, ainsi qu'un test utilisant une mémoire efficace (ne pas allouer un nœud à la fois). Dans les deux cas, je ne pouvais pas vraiment les implémenter en utilisant les conteneurs existants et je devais donc utiliser placement new pour éviter d'appeler de manière superflue des constructeurs et des destructeurs sur des choses inutiles gauche et droite.

Naturellement, si vous travaillez avec des allocateurs personnalisés pour allouer des objets individuellement, comme une liste libre, vous voudrez aussi généralement utiliser placement new, comme ceci (exemple de base qui ne se préoccupe pas d'exception-safety ou de RAII) :

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

Je pense que cela n’a été souligné dans aucune réponse, mais un autre bon exemple et utilisation pour le nouvel emplacement est de réduire la fragmentation de la mémoire (en utilisant des pools de mémoire ) Ceci est particulièrement utile dans les systèmes embarqués et à haute disponibilité. Dans ce dernier cas, c'est particulièrement important car pour un système devant fonctionner 24/365 jours, il est très important de ne pas avoir de fragmentation. Ce problème n'a rien à voir avec une fuite de mémoire.

Même si une très bonne implémentation malloc est utilisée (ou une fonction de gestion de mémoire similaire), il est très difficile de gérer la fragmentation pendant longtemps. À un moment donné, si vous ne gérez pas intelligemment les appels de réservation/libération de mémoire, vous risquez de vous retrouver avec beaucoup de petites lacunes qui sont difficiles à réutiliser (affectez de nouvelles réservations). Ainsi, l’une des solutions utilisées dans ce cas consiste à utiliser un pool de mémoire pour allouer à l’avance la mémoire pour les objets d’application. Après chaque fois que vous avez besoin de mémoire pour un objet, vous utilisez simplement le nouvel emplacement pour créer un nouvel objet dans la mémoire déjà réservée.

De cette façon, une fois votre application démarrée, vous avez déjà toute la mémoire nécessaire réservée. Toute la nouvelle réservation/libération de mémoire va aux pools alloués (vous pouvez avoir plusieurs pools, un pour chaque classe d'objet différente). Aucune fragmentation de la mémoire ne se produit dans ce cas car il n'y aura pas de lacunes et que votre système pourra fonctionner pendant de très longues périodes (années) sans souffrir de fragmentation.

Je l’ai vu dans la pratique spécialement pour le VxWorks RTOS puisque son système d’allocation de mémoire par défaut souffre beaucoup de la fragmentation. Donc, allouer de la mémoire via la nouvelle méthode/malloc standard était fondamentalement interdit dans le projet. Toutes les réservations de mémoire doivent aller à un pool de mémoire dédié.

8
rkachach

Je l'ai utilisé pour stocker des objets avec des fichiers mappés en mémoire.
L’exemple spécifique était une base de données d’images qui traitait un très grand nombre d’images de grande taille (plus que la mémoire ne pouvait en contenir).

7
Martin Beckett

Il est utilisé par std::vector<> parce que std::vector<> alloue généralement plus de mémoire qu'il n'y a de objects dans le vector<>.

7
Andreas Magnusson

Je l'ai vu utilisé comme un léger piratage des performances pour un pointeur de "type dynamique" (dans la section "Sous le capot"):

Mais voici le truc délicat que j’utilisais pour obtenir des performances rapides pour les petits types: si la valeur détenue peut tenir dans un vide *, je ne me donne pas la peine d’allouer un nouvel objet, je le force dans le pointeur lui-même en utilisant placement new .

7
Max Lybbert

Je l'ai utilisé pour créer des objets basés sur la mémoire contenant des messages reçus du réseau.

6
Steve Fallows

En règle générale, le placement nouveau est utilisé pour supprimer le coût d'allocation d'un "nouveau normal".

Un autre scénario où je l'ai utilisé est un endroit où je voulais avoir accès au pointeur à un objet qui était encore à construire, pour mettre en œuvre un singleton par document.

5
xtofl

Cela peut être pratique lors de l’utilisation de la mémoire partagée, entre autres utilisations ... Par exemple: http://www.boost.org/doc/libs/1_51_0/doc/html/interprocess/synchronization_mechanisms.html#interprocess. synchronization_mechanisms.conditions.conditions_anonymous_example

5
kralyk

Les moteurs de script peuvent l'utiliser dans l'interface native pour allouer des objets natifs à partir de scripts. Voir Angelscript (www.angelcode.com/angelscript) pour des exemples.

4
Raindog

Le seul endroit où je l'ai rencontré est dans des conteneurs qui allouent un tampon contigu, puis le remplissent avec des objets selon les besoins. Comme mentionné, std :: vector pourrait faire cela, et je sais que certaines versions de MFC CArray et/ou de CList l'ont fait (parce que c'est là que je l'ai rencontré pour la première fois). La méthode de surallocation de mémoire tampon est une optimisation très utile, et le nouveau placement est pratiquement le seul moyen de construire des objets dans ce scénario. Il est également parfois utilisé pour construire des objets dans des blocs de mémoire alloués en dehors de votre code direct.

Je l'ai utilisé dans une capacité similaire, bien que cela ne soit pas fréquent. C'est un outil utile pour la boîte à outils C++, cependant.

4
Nick

Consultez le fichier fp.h dans le projet xll à l’adresse http://xll.codeplex.com Il résout le problème du "problème injustifié lié au compilateur" pour les tableaux qui aiment transporter leurs dimensions.

 typedef struct _FP 
 {
 unsigned short int rangs; 
 unsigned short int column; 
 tableau double [1];/* En fait, tableau [lignes] [colonnes] */
} FP; 
3
Keith A. Lewis

Voici l'utilisation de tueur pour le constructeur in-situ C++: alignement sur une ligne de cache, ainsi que d'autres puissances de 2 limites. Voici mon algorithme d'alignement de pointeur ultra-rapide pour toute puissance de 2 limites avec 5 instructions à cycle unique ou moins :

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

Maintenant, ne vous contentez-vous pas de sourire (:-). Je ♥♥♥ C++ 1x

2
user2356685