c++ - tipos - Pasar el puntero nulo a la ubicación nueva
punteros y vectores en c (1)
El new
operador de ubicación predeterminado se declara en 18.6 [support.dynamic] ¶1 con una especificación de excepción que no arroja:
void* operator new (std::size_t size, void* ptr) noexcept;
Esta función no hace más que return ptr;
por lo que es razonable que sea noexcept
, sin embargo de acuerdo con 5.3.4 [expr.new] ¶15 esto significa que el compilador debe verificar que no devuelve null antes de invocar el constructor del objeto:
-15-
[ Nota: a menos que una función de asignación se declare con unastd::bad_alloc
excepción que nostd::bad_alloc
(15.4), indica una falla al asignar almacenamiento lanzando una excepciónstd::bad_alloc
(Cláusula 15, 18.6.2.1); devuelve un puntero no nulo en caso contrario. Si la función de asignación se declara con una especificación de excepción que no arroja, devuelve nulo para indicar que no se ha asignado almacenamiento y un puntero no nulo en caso contrario. -Finalizar ] Si la función de asignación devuelve nulo, no se realizará la inicialización, no se invocará la función de desasignación, y el valor de la nueva expresión será nulo.
Me parece que (específicamente para la colocación new
, no en general) esta comprobación nula es un golpe de rendimiento desafortunado, aunque pequeño.
He estado depurando algún código donde la ubicación new
se estaba utilizando en una ruta de código muy sensible al rendimiento para mejorar la generación de código del compilador y se observó la comprobación de nulo en el ensamblado. Al proporcionar una new
sobrecarga de colocación específica de clase que se declara con una especificación de excepción lanzada (aunque posiblemente no se pueda lanzar) se eliminó la rama condicional, lo que también permitió al compilador generar código más pequeño para las funciones en línea circundantes. El resultado de decir que la new
función de ubicación podría arrojar, aunque no pudo , fue un código mensurablemente mejor.
Así que me he estado preguntando si realmente se requiere el cheque nulo para el new
caso de colocación. La única forma en que puede devolver nulo es si lo pasa nulo. Aunque es posible, y aparentemente legal, escribir:
void* ptr = nullptr;
Obj* obj = new (ptr) Obj();
assert( obj == nullptr );
No veo por qué sería útil, sugiero que sería mejor si el programador tuviera que verificar nulo explícitamente antes de usar la ubicación new
por ejemplo,
Obj* obj = ptr ? new (ptr) Obj() : nullptr;
¿Alguna vez alguien necesitó una ubicación new
para manejar correctamente la caja del puntero nulo? (es decir, sin agregar una verificación explícita de que ptr
es una ubicación de memoria válida).
Me pregunto si sería razonable prohibir pasar un puntero nulo a la new
función de ubicación predeterminada, y si no hay una forma mejor de evitar la rama innecesaria, aparte de intentar decirle al compilador que el valor no es nulo, por ejemplo
void* ptr = getAddress();
(void) *(Obj*)ptr; // inform the optimiser that dereferencing pointer is valid
Obj* obj = new (ptr) Obj();
O:
void* ptr = getAddress();
if (!ptr)
__builtin_unreachable(); // same, but not portable
Obj* obj = new (ptr) Obj();
NB: esta pregunta es una microoptimización etiquetada intencionalmente, no estoy sugiriendo que sobrecargues la ubicación new
para todos tus tipos para "mejorar" el rendimiento. Este efecto se notó en un caso muy específico de rendimiento crítico y se basó en el perfil y la medición.
Actualización: DR 1748 hace que el comportamiento indefinido sea utilizar un puntero nulo con la ubicación nueva, por lo que los compiladores ya no están obligados a realizar el control.
Aunque no puedo ver una gran cantidad de preguntas allí, excepto "¿Alguien ha necesitado una ubicación nueva para manejar correctamente el caso del puntero nulo?" (No lo hice), creo que el caso es lo suficientemente interesante como para derramar algunas ideas sobre el tema.
Considero el estándar roto o incompleto con la nueva función de ubicación y los requisitos para las funciones de asignación en general.
Si observa detenidamente el §5.3.4,13 citado, implica que cada función de asignación debe verificarse para un nullpointer devuelto, incluso si no es noexcept
. Por lo tanto, debe ser reescrito para
Si la función de asignación se declara con una especificación de excepción que no arroja y devuelve nulo, no se realizará la inicialización, no se invocará la función de desasignación y el valor de la nueva expresión será nulo.
Eso no dañaría la validez de las funciones de asignación que arrojan excepciones, ya que tienen que obedecer §3.7.4.1 :
[...] Si tiene éxito, devolverá la dirección del inicio de un bloque de almacenamiento cuya longitud en bytes será al menos tan grande como el tamaño solicitado. [...] El puntero devuelto se alineará adecuadamente de modo que se pueda convertir en un puntero de cualquier tipo de objeto completo con un requisito de alineación fundamental (3.11) y luego se utilizará para acceder al objeto o matriz en el almacenamiento asignado (hasta el el almacenamiento es desasignado explícitamente por una llamada a una función de desasignación correspondiente).
Y §5.3.4,14 :
[Nota: cuando la función de asignación devuelve un valor distinto de nulo, debe ser un puntero a un bloque de almacenamiento en el que se ha reservado espacio para el objeto. Se supone que el bloque de almacenamiento está apropiadamente alineado y del tamaño solicitado. [...] -finalización]
Obviamente, una ubicación nueva que simplemente devuelve el puntero dado, no puede verificar razonablemente el tamaño de almacenamiento y la alineación. Por lo tanto,
§18.6.1.3.1 sobre la ubicación nueva dice
[...] Las disposiciones de (3.7.4) no se aplican a estas formas de ubicación reservadas de operador nuevo y eliminación del operador.
(Supongo que se olvidó de mencionar §5.3.4,14 en ese lugar).
Sin embargo, juntos, estos párrafos dicen indirectamente "si pasas un puntero a las funciones de ordenamiento, obtienes UB, porque se viola §5.3.4,14". Por lo tanto, depende de usted comprobar la cordura de cualquier poitner dado a la ubicación nueva.
En ese espíritu, y con el §5.3.4,13 reescrito, el estándar podría despojar al no noexcept
de la colocación nueva, lo que lleva a una adición a esa conclusión indirecta: "... y si pasa nulo, también obtiene UB" . Por otro lado, es mucho menos probable que tenga un puntero desviado o un puntero a muy poca memoria que tener un puntero nulo.
Sin embargo, esto eliminaría la necesidad de verificar contra nulo, y encajaría bien con la filosofía "no pague por lo que no necesita". La función de asignación en sí misma no debería verificarse, porque el §18.6.1.3.1 lo dice explícitamente.
Para redondear las cosas, uno podría considerar agregar una segunda sobrecarga
void* operator new(std::size_t size, void* ptr, const std::nothrow_t&) noexcept;
Lamentablemente, proponer esto al comité es poco probable que resulte en un cambio, ya que rompería el código existente dependiendo de que la ubicación esté bien con punteros nulos.