¿Qué usos hay para la "colocación nueva"?


411

¿Alguien ha usado alguna vez la "ubicación nueva" de C ++? Si es así, ¿para qué? Me parece que solo sería útil en hardware mapeado en memoria.


14
Esta es solo la información que he estado buscando, para llamar a constructores de objetos en grupos de memoria asignados de impulso. (Esperar que estas palabras clave faciliten que alguien las encuentre en el futuro).
Sideshow Bob

2
Se utiliza en el artículo de Wikipedia C ++ 11 en el constructor de una unión.
Hola

@HelloGoodbye, interesante! En el artículo que vinculó, ¿por qué no puede simplemente hacer p = pty usar el operador de asignación de en Pointlugar de hacerlo new(&p) Point(pt)? Me pregunto las diferencias entre los dos. ¿Llamaría el primero operator=a Point, mientras que el segundo llamará al constructor de copia Point? pero todavía no estoy muy claro por qué uno es mejor que el otro.
Andrei-Niculae Petre

@ Andrei-NiculaePetre No he usado una ubicación nueva, pero supongo que debería usarla, junto con el constructor de la copia, si actualmente no tiene un objeto de esa clase, de lo contrario, debe usar el operador de asignación de copia. A menos que la clase sea trivial; entonces no importa cuál de ellos uses. Lo mismo ocurre con la destrucción del objeto. Si no se maneja esto adecuadamente para las clases no triviales, es muy probable que conduzca a un comportamiento extraño, e incluso puede causar un comportamiento indefinido en algunas situaciones.
Hola

@ Andrei-NiculaePetre En realidad, el ejemplo en el artículo de Wikipedia me parece bastante malo, ya que simplemente supone que no existe ningún objeto anterior y que necesitan construir uno. Este no es el caso si U::operator=se acaba de llamar.
HelloGoodbye

Respuestas:


365

La nueva ubicación le permite construir un objeto en la memoria que ya está asignado.

Es posible que desee hacer esto para la optimización cuando necesite construir múltiples instancias de un objeto, y es más rápido no reasignar memoria cada vez que necesite una nueva instancia. En cambio, podría ser más eficiente realizar una única asignación para un trozo de memoria que puede contener múltiples objetos, aunque no quiera usarlos todos a la vez.

DevX da un buen ejemplo :

Standard C ++ también admite la colocación de un nuevo operador, que construye un objeto en un búfer preasignado. Esto es útil cuando se crea un grupo de memoria, un recolector de basura o simplemente cuando el rendimiento y la seguridad de excepción son primordiales (no hay peligro de falla de asignación ya que la memoria ya ha sido asignada, y construir un objeto en un búfer preasignado lleva menos tiempo) :

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

También es posible que desee asegurarse de que no puede haber un error de asignación en una determinada parte del código crítico (por ejemplo, en el código ejecutado por un marcapasos). En ese caso, desearía asignar memoria antes, luego usar la ubicación nueva dentro de la sección crítica.

Desasignación en colocación nuevo

No debe desasignar todos los objetos que usan el búfer de memoria. En su lugar, debe eliminar [] solo el búfer original. Tendría que llamar a los destructores de sus clases manualmente. Para una buena sugerencia sobre esto, consulte las Preguntas frecuentes de Stroustrup sobre: ¿Hay una "eliminación de ubicación" ?


54
No está en desuso, ya que necesita esta función para implementar eficientemente objetos de contenedor (como vector). Sin embargo, si no está creando su propio contenedor, no necesita usar esta función.
Martin York

26
También es muy importante recordar #incluir <memoria>, de lo contrario, podría encontrar algunos terribles dolores de cabeza en algunas plataformas que no reconocen automáticamente la ubicación nueva
Ramon Zarazua B.

22
Estrictamente, es un comportamiento indefinido llamar delete[]al charbúfer original . El uso de la colocación newha finalizado la vida útil de los charobjetos originales al reutilizar su almacenamiento. Si ahora llama delete[] bufal tipo dinámico de los objetos apuntados, ya no coincide con su tipo estático, entonces tiene un comportamiento indefinido. Es más consistente usar operator new/ operator deleteasignar memoria cruda destinada para su uso por ubicación new.
CB Bailey

31
Definitivamente omitiría usar el montón en un marcapasos :-)
Eli Bendersky

15
@RamonZarazua Encabezado incorrecto, es #include <new>.
bit2shift

63

Lo usamos con grupos de memoria personalizados. Solo un boceto:

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

Ahora puede agrupar objetos en una sola arena de memoria, seleccionar un asignador que sea muy rápido pero no desasigna, use el mapeo de memoria y cualquier otra semántica que desee imponer eligiendo el grupo y pasándolo como argumento a la ubicación de un objeto nuevo operador


1
Sí. Nos volvemos bastante inteligentes al respecto, pero está fuera de tema para esta pregunta.
Don Wakefield

2
@jdkoftinoff ¿tiene algún enlace a una muestra de código real? me parece bastante interesante!
Victor

@DonWakefield ¿Cómo manejas la alineación en este grupo? ¿No deberías pasar la alineación como argumento a allocate()alguna parte?
Mikhail Vasilyev

1
@MikhailVasilyev, en una implementación real, por supuesto, manejarías eso. Código de ejemplo solamente.
Don Wakefield

¿Qué pasa si la ubicación es una dirección no válida, digamos 0x0?
Charlie

51

Es útil si desea separar la asignación de la inicialización. STL utiliza la colocación nueva para crear elementos de contenedor.


35

Lo he usado en programación en tiempo real. Por lo general , no queremos realizar ninguna asignación dinámica (o desasignación) después de que se inicie el sistema, porque no hay garantía de cuánto tiempo llevará eso.

Lo que puedo hacer es preasignar una gran cantidad de memoria (lo suficientemente grande como para contener cualquier cantidad de lo que la clase pueda requerir). Luego, una vez que descubro en el tiempo de ejecución cómo construir las cosas, la colocación nueva se puede usar para construir objetos justo donde los quiero. Una situación en la que sé que lo usé fue para ayudar a crear un búfer circular heterogéneo .

Ciertamente no es para los débiles de corazón, pero es por eso que hacen que la sintaxis sea un poco retorcida.


Hola TED, ¿podría compartir más sobre la solución que tiene? Estoy pensando en una solución preasignada, pero no he progresado mucho. ¡Gracias de antemano!
Viet

1
Bueno, el código de búfer circular hetrogenioso real fue realmente la parte difícil de acertar. El nuevo lugar se ve un poco espeluznante, pero en comparación no fue un problema en absoluto.
TED

26

Lo he usado para construir objetos asignados en la pila a través de alloca ().

enchufe descarado: escribí un blog al respecto aquí .


artículo interesante, pero no estoy seguro de entender la ventaja de usar esto boost::array. ¿Puedes ampliar eso un poco?
GrahamS

boost :: array requiere que el tamaño de la matriz sea una constante de tiempo de compilación. Esto no tiene esa limitación.
Ferruccio

2
@Ferruccio Esto es bastante bueno, noté que tu macro es un poco insegura, es decir, el tamaño podría ser una expresión. Si se pasa x + 1, por ejemplo, lo expandiría a sizeof (type) * x + 1, lo que sería incorrecto. Necesitas agrupar tu macro para que sea más segura.
Benj

El uso con alloca me parece peligroso si se produce una excepción, ya que debe llamar a los destructores en todos sus objetos.
CashCow

14

Head Geek: ¡BINGO! Lo tienes totalmente, eso es exactamente para lo que es perfecto. En muchos entornos integrados, las restricciones externas y / o el escenario de uso general obligan al programador a separar la asignación de un objeto de su inicialización. Juntos, C ++ llama a esto "instanciación"; pero siempre que la acción del constructor se debe invocar explícitamente SIN asignación dinámica o automática, la colocación nueva es la forma de hacerlo. También es la manera perfecta de localizar un objeto global de C ++ que está anclado a la dirección de un componente de hardware (E / S mapeadas en memoria), o para cualquier objeto estático que, por cualquier razón, debe residir en una dirección fija.


12

Lo he usado para crear una clase Variant (es decir, un objeto que puede representar un valor único que puede ser uno de varios tipos diferentes).

Si todos los tipos de valores admitidos por la clase Variant son tipos POD (por ejemplo, int, float, double, bool), entonces una unión de estilo C etiquetada es suficiente, pero si desea que algunos de los tipos de valor sean objetos C ++ ( por ejemplo, std :: string), la función de unión C no funcionará, ya que los tipos de datos que no son POD pueden no declararse como parte de una unión.

Entonces, en su lugar, asigno una matriz de bytes que es lo suficientemente grande (por ejemplo, sizeof (the_largest_data_type_I_support)) y uso la ubicación nueva para inicializar el objeto C ++ apropiado en esa área cuando la Variante está configurada para contener un valor de ese tipo. (Y la eliminación de la ubicación de antemano al cambiar de un tipo de datos diferente a POD, por supuesto)


Erm, los tipos de datos que no son POD se pueden declarar dentro de una unión, siempre que proporcione un ctor de unión, y bueno, ese ctor probablemente usaría la ubicaciónnew para inicializar su subclase no POD. Ref: stackoverflow.com/a/33289972/2757035 Reinventar esta rueda usando una matriz de bytes arbitrariamente grande es una impresionante pieza de acrobacia, pero parece totalmente innecesaria. Entonces, ¿qué me he perdido? :)
underscore_d

66
Te perdiste todas las versiones de C ++ antes de C ++ 11, que en muchos casos aún necesitan ser compatibles. :)
Jeremy Friesner

10

La colocación nueva también es muy útil cuando se serializa (digamos con boost :: serialization). En 10 años de c ++, este es solo el segundo caso para el que necesito una nueva ubicación (tercero si incluye entrevistas :)).


9

También es útil cuando desea reinicializar estructuras globales o estáticamente asignadas.

La antigua forma de C se usaba memset()para establecer todos los elementos en 0. No se puede hacer eso en C ++ debido a vtables y constructores de objetos personalizados.

Así que a veces uso lo siguiente

 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
¿No necesitarías hacer una destrucción correspondiente antes de reiniciarlo de esa manera?
Head Geek

[Editado para deletrear] Por lo general, lo haces. Pero a veces, cuando sabe que la clase no asigna memoria u otros recursos (o los desasigna externamente, por ejemplo, cuando usa grupos de memoria), puede usar esta técnica. Garantiza que los punteros de v-table no se sobrescriban. - nimrodm Hace 16 horas
nimrodm

1
Incluso en C, el uso de establecer todos los bits en 0 solo se garantiza para producir una representación de 0 para los tipos integrales, no para otros tipos (el puntero nulo puede tener una representación distinta de cero).
curioso

@curiousguy: para los tipos primitivos tiene razón (hará que el programa sea predecible, lo cual es una ventaja cuando se trata de depuración). Sin embargo, los tipos de datos C ++ tendrán su constructor ejecutado (en el lugar) y se inicializarán correctamente.
nimrodm

9

Creo que esto no ha sido resaltado por ninguna respuesta, pero otro buen ejemplo y uso para la nueva ubicación es reducir la fragmentación de la memoria (mediante el uso de grupos de memoria). Esto es especialmente útil en sistemas integrados y de alta disponibilidad. En este último caso es especialmente importante porque para un sistema que tiene que funcionar 24/365 días es muy importante no tener fragmentación. Este problema no tiene nada que ver con la pérdida de memoria.

Incluso cuando se utiliza una muy buena implementación de malloc (o una función de administración de memoria similar) es muy difícil lidiar con la fragmentación durante mucho tiempo. En algún momento, si no gestiona inteligentemente las llamadas de reserva / liberación de memoria, podría terminar con muchos pequeños vacíos que son difíciles de reutilizar (asignar a nuevas reservas). Por lo tanto, una de las soluciones que se utilizan en este caso es utilizar un grupo de memoria para asignar de antemano la memoria para los objetos de la aplicación. Después, cada vez que necesite memoria para algún objeto, simplemente use la nueva ubicación para crear un nuevo objeto en la memoria ya reservada.

De esta manera, una vez que se inicia su aplicación, ya tiene toda la memoria necesaria reservada. Toda la nueva reserva / liberación de memoria va a los grupos asignados (puede tener varios grupos, uno para cada clase de objeto diferente). En este caso, no ocurre fragmentación de la memoria, ya que no habrá brechas y su sistema puede funcionar durante períodos muy largos (años) sin sufrir fragmentación.

Vi esto en la práctica especialmente para el VxWorks RTOS ya que su sistema de asignación de memoria predeterminado sufre mucho de fragmentación. Por lo tanto, la asignación de memoria a través del método estándar nuevo / malloc estaba básicamente prohibido en el proyecto. Todas las reservas de memoria deben ir a un grupo de memoria dedicado.


9

En realidad, es necesario implementar cualquier tipo de estructura de datos que asigne más memoria que la mínimamente requerida para el número de elementos insertados (es decir, cualquier otra cosa que no sea una estructura vinculada que asigna un nodo a la vez).

Contenedores para llevar gusta unordered_map, vectoro deque. Todos estos asignan más memoria de la mínima requerida para los elementos que ha insertado hasta ahora para evitar requerir una asignación de montón para cada inserción. Usemos vectorcomo el ejemplo más simple.

Cuando tu lo hagas:

vector<Foo> vec;

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

... eso en realidad no construye mil Foos. Simplemente asigna / reserva memoria para ellos. Si vectorno usó la ubicación nueva aquí, sería una construcción predeterminada en Foostodo el lugar, así como tener que invocar sus destructores incluso para elementos que nunca insertó en primer lugar.

Asignación! = Construcción, Liberación! = Destrucción

En términos generales, para implementar muchas estructuras de datos como las anteriores, no puede tratar la asignación de memoria y la construcción de elementos como una cosa indivisible, y tampoco puede tratar la liberación de memoria y la destrucción de elementos como una cosa indivisible.

Tiene que haber una separación entre estas ideas para evitar invocar innecesariamente constructores y destructores innecesariamente a izquierda y derecha, y es por eso que la biblioteca estándar separa la idea de std::allocator(que no construye ni destruye elementos cuando asigna / libera memoria *) los contenedores que lo usan que construyen elementos manualmente mediante la colocación de elementos nuevos y destruyen elementos manualmente mediante invocaciones explícitas de destructores.

  • Odio el diseño de, std::allocatorpero ese es un tema diferente sobre el que evitaré despotricar. :-RE

De todos modos, tiendo a usarlo mucho ya que he escrito una serie de contenedores C ++ de propósito general que cumplen con los estándares que no se pudieron construir en términos de los existentes. Entre ellos se incluye una pequeña implementación de vectores que construí hace un par de décadas para evitar las asignaciones de almacenamiento dinámico en casos comunes, y un trie de memoria eficiente (no asigna un nodo a la vez). En ambos casos, realmente no pude implementarlos usando los contenedores existentes, por lo que tuve que usar placement newpara evitar invocar superfluos constructores y destructores en cosas innecesarias a izquierda y derecha.

Naturalmente, si alguna vez trabaja con asignadores personalizados para asignar objetos individualmente, como una lista gratuita, entonces generalmente también querrá usar placement new, como este (ejemplo básico que no molesta con la seguridad de excepción o RAII):

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

8

Es útil si está construyendo un núcleo: ¿dónde coloca el código del núcleo que leyó del disco o de la tabla de páginas? Necesitas saber a dónde saltar.

O en otras circunstancias muy raras, como cuando tiene un montón de espacio asignado y desea colocar algunas estructuras una detrás de la otra. Se pueden empaquetar de esta manera sin la necesidad del operador offsetof (). Sin embargo, también hay otros trucos para eso.

También creo que algunas implementaciones de STL hacen uso de la colocación nueva, como std :: vector. Asignan espacio para 2 ^ n elementos de esa manera y no necesitan reasignar siempre.


La reducción de las asignaciones de memoria es una razón principal para usarla, así como "trucos" como cargar objetos del disco
lefticus

No conozco ningún núcleo escrito en C ++; la mayoría de los granos están escritos en C.
Adam Rosenfield

8
El sistema operativo con el que aprendí lo básico del sistema operativo está escrito en C ++: sweb.sourceforge.net
mstrobl

8

Se usa std::vector<>porque std::vector<>normalmente asigna más memoria de la que hay objectsen el vector<>.


7

Lo he usado para almacenar objetos con archivos asignados en memoria.
El ejemplo específico era una base de datos de imágenes que procesaba una gran cantidad de imágenes grandes (más de las que cabían en la memoria).


7

Lo he visto como un ligero truco de rendimiento para un puntero de "tipo dinámico" (en la sección "Bajo el capó"):

Pero aquí está el truco complicado que utilicé para obtener un rendimiento rápido para tipos pequeños: si el valor que se mantiene puede caber dentro de un vacío *, en realidad no me molesto en asignar un nuevo objeto, lo fuerzo al puntero en sí mismo usando la colocación nueva .


¿Qué significa si el valor retenido puede caber dentro de un vacío * significa? Siempre es posible asignar cualquier tipo de puntero a void *. ¿Puedes por favor mostrarnos algún ejemplo?
anurag86

@ anurag86: en mi máquina de 64 bits, un void*toma 8 bytes. Es un poco tonto señalar un byte de ocho void*en un byte bool. Pero es completamente posible superponerlo boolen el void*, al igual que a union { bool b; void* v }. Necesita alguna forma de saber que lo que llamó a void*es en realidad un bool(o un short, o un float, etc.). El artículo al que me vinculé describe cómo hacerlo. Y, para responder a la pregunta original, la ubicación newes la característica utilizada para crear un bool(u otro tipo) donde void*se espera un (los modelos se utilizan para obtener / modificar el valor más adelante).
Max Lybbert

@ anurag86: No es lo mismo, pero puede que le interesen los punteros etiquetados ( en.wikipedia.org/wiki/Tagged_pointer ).
Max Lybbert

6

Lo he usado para crear objetos basados ​​en la memoria que contiene mensajes recibidos de la red.


5

En general, la colocación nueva se utiliza para deshacerse del costo de asignación de una 'nueva normal'.

Otro escenario donde lo usé es un lugar donde quería tener acceso al puntero a un objeto que aún estaba por construirse, para implementar un singleton por documento.



4

El único lugar donde me he encontrado es en contenedores que asignan un búfer contiguo y luego lo llenan con objetos según sea necesario. Como se mencionó, std :: vector podría hacer esto, y sé que algunas versiones de MFC CArray y / o CList hicieron esto (porque ahí es donde lo encontré por primera vez). El método de sobreasignación de búfer es una optimización muy útil, y la colocación nueva es prácticamente la única forma de construir objetos en ese escenario. También se usa a veces para construir objetos en bloques de memoria asignados fuera de su código directo.

Lo he usado en una capacidad similar, aunque no aparece a menudo. Sin embargo, es una herramienta útil para la caja de herramientas de C ++.


4

Los motores de script pueden usarlo en la interfaz nativa para asignar objetos nativos a partir de scripts. Consulte Angelscript (www.angelcode.com/angelscript) para ver ejemplos.


3

Consulte el archivo fp.h en el proyecto xll en http://xll.codeplex.com. Resuelve el problema de "falta de garantía injustificada con el compilador" para las matrices que desean llevar sus dimensiones consigo.

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

2

Aquí está el uso asesino para el constructor in situ de C ++: alineación a una línea de caché, así como a otras potencias de 2 límites. Aquí está mi algoritmo de alineación de puntero ultrarrápido a cualquier potencia de 2 límites con 5 o menos instrucciones de ciclo único :

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

Ahora, eso no solo pone una sonrisa en tu cara (:-). I ♥♥♥ C ++ 1x

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.