c++ memory-management standards placement-new

c++ - ¿Colocación de matriz nueva requiere una sobrecarga no especificada en el búfer?



memory-management standards (6)

5.3.4 [expr.new] del borrador C ++ 11 Feb da el ejemplo:

new(2,f) T[5] da como resultado una llamada del operator new[](sizeof(T)*5+y,2,f) .

Aquí, xey son valores no específicos no negativos que representan la sobrecarga de asignación de matriz; el resultado de la nueva expresión se compensará con esta cantidad del valor devuelto por el operator new[] . Esta sobrecarga se puede aplicar en todas las expresiones nuevas de matriz, incluidas las que hacen referencia al operator new[](std::size_t, void*) función de biblioteca operator new[](std::size_t, void*) y otras funciones de asignación de ubicación. La cantidad de sobrecarga puede variar de una invocación de nueva a otra. -Final ejemplo ]

Ahora toma el siguiente código de ejemplo:

void* buffer = malloc(sizeof(std::string) * 10); std::string* p = ::new (buffer) std::string[10];

De acuerdo con la cita anterior, la segunda línea new (buffer) std::string[10] llamará internamente al operator new[](sizeof(std::string) * 10 + y, buffer) (antes de construir el std::string individual std::string objetos de std::string ). El problema es que si y > 0 , el búfer preasignado será demasiado pequeño.

Entonces, ¿cómo puedo saber cuánta memoria preasignar al usar la colocación de la matriz-nueva?

void* buffer = malloc(sizeof(std::string) * 10 + how_much_additional_space); std::string* p = ::new (buffer) std::string[10];

¿O el estándar en alguna parte garantiza que y == 0 en este caso? De nuevo, la cita dice:

Esta sobrecarga se puede aplicar en todas las expresiones nuevas de matriz, incluidas las que hacen referencia al operator new[](std::size_t, void*) función de biblioteca operator new[](std::size_t, void*) y otras funciones de asignación de ubicación.


Esta sobrecarga se puede aplicar en todas las expresiones nuevas de matriz, incluidas las que hacen referencia al operator new[](std::size_t, void*) función de biblioteca operator new[](std::size_t, void*) y otras funciones de asignación de ubicación.

Esto es un defecto en el estándar. Se rumorea que no pudieron encontrar un voluntario para escribir una excepción (Mensaje n. ° 1165).

La disposición de la matriz no reemplazable new no se puede usar con expresiones delete[] , por lo que debe recorrer la matriz y llamar a cada destructor .

La sobrecarga se dirige a la ubicación de la matriz definida por el usuario, nuevas funciones que asignan la memoria al igual que la T* tp = new T[length] regular T* tp = new T[length] . Esos son compatibles con delete[] , de ahí la sobrecarga que lleva la longitud de la matriz.


Como mencionó Kerrek SB en los comentarios, este defecto se informó por primera vez en 2004 , y se resolvió en 2012 como:

El CWG acordó que el EWG es el lugar apropiado para tratar este tema.

Luego, el defecto fue reportado a EWG en 2013 , pero se cerró como NAD (presumiblemente significa "No un defecto") con el comentario:

El problema está en tratar de usar una matriz nueva para colocar una matriz en el almacenamiento preexistente. No necesitamos usar una matriz nueva para eso; solo construirlos.

lo que presumiblemente significa que la solución sugerida es usar un ciclo con una llamada a una ubicación que no sea una matriz nueva para cada objeto que se está construyendo.

Un corolario no mencionado en otra parte del hilo es que este código causa un comportamiento indefinido para todos los T :

T *ptr = new T[N]; ::operator delete[](ptr);

Incluso si cumplimos con las reglas de vida (es decir, si T tiene destrucción trivial o si el programa no depende de los efectos secundarios del destructor), el problema es que ptr ha sido ajustado para esta cookie no especificada, por lo que es un valor incorrecto para pasar al operator delete[] .


Después de leer las secciones estándar correspondientes, estoy comenzando a pensar que la colocación nueva para los tipos de matriz es simplemente una idea inútil, y la única razón por la que se permite por norma es la forma genérica en que se describe al nuevo operador:

La nueva expresión intenta crear un objeto del typeid (8.1) o newtypeid al que se aplica. El tipo de ese objeto es el tipo asignado. Este tipo será un tipo de objeto completo, pero no un tipo de clase abstracta o una matriz de los mismos (1.8, 3.9, 10.4). [Nota: debido a que las referencias no son objetos, las referencias no pueden ser creadas por newexpressions. ] [Nota: el typeid puede ser un tipo cvalified, en cuyo caso el objeto creado por la nueva expresión tiene un tipo cvalified. ]

new-expression: ::(opt) new new-placement(opt) new-type-id new-initializer(opt) ::(opt) new new-placement(opt) ( type-id ) new-initializer(opt) new-placement: ( expression-list ) newtypeid: type-specifier-seq new-declarator(opt) new-declarator: ptr-operator new-declarator(opt) direct-new-declarator direct-new-declarator: [ expression ] direct-new-declarator [ constant-expression ] new-initializer: ( expression-list(opt) )

Para mí, parece que la array placement new simplemente se deriva de la compacidad de la definición (todos los usos posibles como un solo esquema), y parece que no hay una buena razón para que esté prohibida.

Esto nos deja en una situación en la que tenemos un operador inútil, que necesita memoria asignada antes de saber cuánto se necesitará. Las únicas soluciones que veo serían sobreasignar la memoria y esperar que el compilador no desee más que proporcionar, o reasignar memoria en la array placement new función / método de array placement new (lo que más bien frustra el propósito de usar la array placement new en primer lugar )

Para responder la pregunta señalada por Kerrek SB: Su ejemplo:

void * addr = std::malloc(N * sizeof(T)); T * arr = ::new (addr) T[N]; // #1

no siempre es correcto En la mayoría de las implementaciones, arr!=addr (y hay buenas razones para ello), por lo que su código no es válido, y su memoria intermedia será rebasada.

Acerca de esas "buenas razones": tenga en cuenta que los creadores estándar las liberan de algunas tareas domésticas cuando utilizan el array new operador de array placement new , y la array placement new no es diferente a este respecto. Tenga en cuenta que no es necesario que informe delete[] sobre la longitud de la matriz, por lo que esta información debe mantenerse en la misma matriz. ¿Dónde? Exactamente en esta memoria extra. Sin ella, delete[] ''ing requeriría mantener la longitud de la matriz por separado (como stl hace usando bucles y no coloca new )


Llamar a cualquier versión del operator new[] () no funcionará muy bien con un área de memoria de tamaño fijo. Esencialmente, se supone que delega a alguna función de asignación de memoria real en lugar de simplemente devolver un puntero a la memoria asignada. Si ya tiene un campo de memoria en el que desea construir una matriz de objetos, quiere usar std::uninitialized_fill() o std::uninitialized_copy() para construir los objetos (o alguna otra forma de construir individualmente los objetos).

Podría argumentar que esto significa que también tiene que destruir manualmente los objetos en la arena de su memoria. Sin embargo, llamar a delete[] array en el puntero devuelto desde la ubicación new no funcionará: usaría la versión sin ubicación del operator delete[] () ! Es decir, cuando utiliza la ubicación new , necesita destruir manualmente los objetos y liberar la memoria.


No use el operator new[](std::size_t, void* p) menos que conozca a priori la respuesta a esta pregunta. La respuesta es un detalle de implementación y puede cambiar con el compilador / plataforma. Aunque es típicamente estable para cualquier plataforma dada. Por ejemplo, esto es algo especificado por Itanium ABI .

Si no conoce la respuesta a esta pregunta, escriba su propia matriz de ubicaciones nueva que pueda verificar esto en tiempo de ejecución:

inline void* operator new[](std::size_t n, void* p, std::size_t limit) { if (n <= limit) std::cout << "life is good/n"; else throw std::bad_alloc(); return p; } int main() { alignas(std::string) char buffer[100]; std::string* p = new(buffer, sizeof(buffer)) std::string[3]; }

Al variar el tamaño de la matriz e inspeccionar n en el ejemplo anterior, puede inferir y para su plataforma. Para mi plataforma y es 1 palabra. El tamaño de (palabra) varía según si estoy compilando para una arquitectura de 32 o 64 bits.


Actualización: después de un debate, entiendo que mi respuesta ya no se aplica a la pregunta. Lo dejo aquí, pero definitivamente se necesita una respuesta real.

Estaré encantado de apoyar esta pregunta con algo de recompensa si no se encuentra una buena respuesta pronto.

Voy a replantear la pregunta aquí hasta donde yo lo entiendo, esperando que una versión más corta pueda ayudar a otros a entender lo que se está preguntando. La pregunta es:

¿La siguiente construcción es siempre correcta? ¿ arr == addr al final?

void * addr = std::malloc(N * sizeof(T)); T * arr = ::new (addr) T[N]; // #1

Sabemos por el estándar que # 1 causa el ::operator new[](???, addr) call ::operator new[](???, addr) , donde ??? es un número no especificado no menor que N * sizeof(T) , y también sabemos que esa llamada solo devuelve addr y no tiene otros efectos. También sabemos que arr está compensado de manera correspondiente con addr . Lo que no sabemos es si la memoria apuntada por addr es suficientemente grande o cómo sabríamos cuánta memoria asignar.

Pareces confundir algunas cosas:

  1. Su ejemplo llama al operator new[]() , no al operator new() .

  2. Las funciones de asignación no construyen nada. Ellos asignan .

Lo que ocurre es que la expresión T * p = new T[10]; causas:

  1. una llamada al operator new[]() con el argumento de tamaño 10 * sizeof(T) + x ,

  2. diez llamadas al constructor predeterminado de T , efectivamente ::new (p + i) T() .

La única peculiaridad es que la nueva expresión de matriz solicita más memoria que la que utilizan los datos de la matriz. Usted no ve nada de esto y no puede hacer uso de esta información de otra manera que no sea la aceptación silenciosa.

Si tiene curiosidad por la cantidad de memoria asignada realmente, puede simplemente reemplazar el operator new[] funciones de asignación de matriz operator new[] y operator delete[] y hacer que se imprima el tamaño real.

Actualización: como parte de la información al azar, debe tener en cuenta que las nuevas funciones de ubicación global deben ser no operativas. Es decir, cuando construyes un objeto o matriz en el lugar de la siguiente manera:

T * p = ::new (buf1) T; T * arr = ::new (buf10) T[10];

Entonces las llamadas correspondientes a ::operator new(std::size_t, void*) y ::operator new[](std::size_t, void*) no hacen más que devolver su segundo argumento. Sin embargo, no sabes a qué se supone que buf10 apunta: debe apuntar a 10 * sizeof(T) + y bytes de memoria, pero no puedes saber y .