17

Solo por curiosidad, ¿es legal lo siguiente?Operador de mezcla nuevo [] y colocación nuevo con eliminación normal []

X* p = static_cast<X*>(operator new[](3 * sizeof(X))); 
new(p + 0) X(); 
new(p + 1) X(); 
new(p + 2) X(); 

delete[] p; // Am I allowed to use delete[] here? Or is it undefined behavior? 

mismo modo:

X* q = new X[3](); 

(q + 2)->~X(); 
(q + 1)->~X(); 
(q + 0)->~X(); 
operator delete[](q); 
+0

Incluso si funciona (lo cual dudo). Hace que el código no se pueda mantener. Cualquier modificación a la clase X (como la adición de operador nuevo) va a necesitar conocer el código anterior al menos para fines de prueba. Esta fuerte unión del uso de X a la implementación de X no es deseable. –

Respuesta

7

Estoy bastante seguro de ambos dan UB.

El §5.3.4/12 dice que la forma de matriz de una nueva expresión puede agregar cierta cantidad arbitraria de sobrecarga a la cantidad de memoria asignada. La matriz de eliminación puede/podría hacer algo con la memoria extra que espera que esté allí, pero no lo es, ya que no asignó el espacio adicional que espera. Por lo menos, al menos normalmente va a compensar la cantidad de memoria adicional que esperaba asignar para volver a la dirección que cree que fue devuelta desde operator new, pero dado que no ha asignado memoria adicional o aplicado un desplazamiento, cuando lo haga pasará un puntero a operator delete[] que no fue devuelto desde operator new[], lo que lleva a UB (y, de hecho, incluso intenta formar la dirección antes de que el comienzo de la dirección devuelta sea técnicamente UB).

La misma sección dice que si asigna memoria adicional, tiene que compensar el puntero devuelto por la cantidad de esa sobrecarga. Cuando/si llama al operator delete[] con el puntero que se devolvió de la nueva expresión sin compensar el desplazamiento, está llamando al operator delete[] con un puntero que es diferente del que se devolvió operator new[], dando a UB nuevamente.

§5.3.4/12 es una nota no normativa, pero no veo nada en el texto normativo que la contradiga.

5

De 5.3.5 [expr.delete] en n3242:

[...]

En la segunda alternativa (eliminar matriz), el valor del operando de eliminar puede ser un valor de puntero nulo o un valor de puntero que resultó de una nueva expresión de matriz anterior . Si no, el comportamiento no está definido. [...]

que significa que para delete[] p, p debe haber sido el resultado de algo de la forma new[] p (una nueva expresión), o 0. En vista de que el resultado de operator new no aparece aquí, yo Creo que el primer caso es correcto.


Creo que el segundo caso está bien. De 18.6.1.2 [new.delete.array]:

void operator delete[](void* ptr) noexcept;

[...]

Requiere: PTR será un puntero nulo o su valor será el valor devuelto por una llamada anterior al operador nuevo o operador de nuevo [] (std :: size_t, const std :: nothrow_t &) que no ha sido invalidada por una llamada a intervenir operador de eliminar. [...]

(hay un texto similar en 3.7.4.2 [basic.stc.dynamic.deallocation], párrafo 3)

Así que, mientras el partido/funciones de asignación (por ejemplo delete[] (new[3] T) está bien formado) no pasa nada malo. [o lo hace? ver más abajo]


Creo que localicé el texto normativo de lo que Jerry está advirtiendo, en 5.3.4 [expr.nuevo]:

Una nueva expresión pasa la cantidad de espacio solicitado a la función de asignación como el primer argumento de tipo std :: size_t. Ese argumento no será menor que el tamaño del objeto que se creó ; puede ser mayor que el tamaño del objeto que se está creando solo si el objeto es una matriz. [...]

Seguir en el mismo párrafo es un ejemplo (por lo tanto no normativo) que subraya que las nuevas expresiones de una implementación son de hecho más libres de pedir más de la función de asignación que el espacio que la matriz (el almacenamiento del parámetro opcional std::size_t disponible para la función de desasignación viene a la mente), y que pueden compensarse en el resultado. Entonces todas las apuestas están desactivadas en el caso de matriz. El caso no es un array parece bien aunque:

auto* p = new T; 
// Still icky 
p->~T(); 
operator delete(p); 
+0

El segundo caso no está bien porque usa un objeto después de haber sido destruido. –

+0

@BenVoigt ¿Qué objeto sería eso? –

+0

@Luc: Acabo de encontrar el párrafo, lo cité en la parte inferior de mi respuesta. –

2

Creo que eso no puede ser legal. Porque eso implica estas ecuaciones:

new-expression = allocation-function + constructor 
delete-expression = destructor + deallocation-function 

Nada más y nada menos. Pero el estándar dice no dicen exactamente eso, hasta donde yo sé. Es posible que new-expression haga más de allocation-function + constructor juntos. Es decir, las ecuaciones reales podrían ser esto, y la Norma no prohíbe explícitamente cualquier lugar:

new-expression = allocation-function + constructor + some-other-work 
delete-expression = destructor + deallocation-function + some-other-work 
2

Si no lo son UB, que deberían ser. En el ejemplo 1 está utilizando delete[] donde el mecanismo subyacente no tiene idea de cuántos objetos se van a destruir. Si la implementación de new[] y delete[] usa cookies, esto fallará. El código en el ejemplo 2 asume que la dirección q es la dirección correcta para pasar al operator delete[], y este no es el caso en una implementación que usa cookies.

+0

+1, solo pensar en las cookies es claramente la forma más fácil de entender la validez. Solo para aclarar, cuando existen las cookies, las agrega el compilador; las funciones 'operator new []' y 'operator delete []' no son las más acertadas. – Potatoswatter

2

correcta sería:

X* p = static_cast<X*>(new char[3 * sizeof(X)]); 
// ... 
delete[] static_cast<char*>(p); 

o

X* p = static_cast<X*>(operator new[](3 * sizeof(X))); 
// ... 
operator delete[](p); 

El tipo de la matriz eliminar la expresión tiene que coincidir con la nueva expresión exactamente.


El primer ejemplo es UB porque la sección 5.3.5 ([expr.delete]) dice

En la primera alternativa (objeto de eliminación), si el tipo estático del objeto a ser eliminado es diferente desde su tipo dinámico, el tipo estático debe ser una clase base del tipo dinámico del objeto a eliminar y el tipo estático debe tener un destructor virtual o el comportamiento no está definido. En la segunda alternativa (eliminar matriz) si el tipo dinámico del objeto a eliminar difiere de su tipo estático, el comportamiento no está definido.


Mi versión corregida es bueno porque (sección 3.9 [basic.life]):

Un programa puede poner fin a la vida de cualquier objeto mediante la reutilización del almacenamiento, que ocupa el objeto o mediante una llamada explícita al destructor para un objeto de un tipo de clase con un destructor no trivial. Para un objeto de un tipo de clase con un destructor no trivial, no es necesario que el programa llame al destructor explícitamente antes de que se reutilice o libere el almacenamiento que ocupa el objeto; sin embargo, si no hay una llamada explícita al destructor o si una expresión de eliminación (5.3.5) no se utiliza para liberar el almacenamiento, el destructor no se llamará implícitamente y cualquier programa que dependa de los efectos secundarios producidos por el destructor ha definido el comportamiento .


El segundo ejemplo no se permite si y sólo si X tiene un destructor no trivial porque (también 3,9 [basic.life]):

antes de que haya comenzado el tiempo de vida de un objeto, pero después de que el almacenamiento de los que el el objeto ocupará ha sido asignado 38 o, después de que la vida útil de un objeto haya finalizado y antes de que el almacenamiento ocupado sea reutilizado o liberado, cualquier puntero que haga referencia a la ubicación de almacenamiento donde se ubicará el objeto se puede usar, pero solo de manera limitada. Para un objeto en construcción o destrucción, ver 12.7. De lo contrario, dicho puntero se refiere al almacenamiento asignado (3.7.4.2), y utilizando el puntero como si el puntero fuera del tipo void*, está bien definido. Tal puntero puede desreferenciarse, pero el valor l resultante solo se puede usar en formas limitadas , como se describe a continuación.

El programa ha indefinido comportamiento si:

  • será el objeto o era de un tipo de clase con un destructor no trivial y el puntero se utiliza como el operando de un delete-expresión,
+0

¿Está seguro de que 'operator new char [] (n)' es una sintaxis válida? – fredoverflow

+0

@Fred: no, no es por supuesto. Me perdí que la pregunta era llamar a la función 'operator new []', pensé que se suponía que era una expresión 'new []' y me faltaba el tipo. –

+0

'operator delete' no es una' delete-expression', es una función de desasignación. –

Cuestiones relacionadas