¿Es seguro eliminar un puntero vacío?

96

Supongamos que tengo el siguiente código:

void* my_alloc (size_t size)
{
   return new char [size];
}

void my_free (void* ptr)
{
   delete [] ptr;
}

¿Es esto seguro? ¿O se debe ptrconvertir char*antes de eliminarlo?

Andrés
fuente
¿Por qué gestiona la memoria usted mismo? ¿Qué estructura de datos estás creando? La necesidad de realizar una gestión de memoria explícita es bastante rara en C ++; Por lo general, debe usar clases que lo manejen por usted desde STL (o desde Boost en un apuro).
Brian
6
Solo para las personas que leen, uso variables void * como parámetros para mis hilos en win c ++ (consulte _beginthreadex). Por lo general, señalan claramente las clases.
ryansstack
3
En este caso, es un contenedor de uso general para nuevo / eliminar, que podría contener estadísticas de seguimiento de asignación o un grupo de memoria optimizado. En otros casos, he visto punteros de objeto almacenados incorrectamente como variables de miembro void * y eliminados incorrectamente en el destructor sin volver al tipo de objeto apropiado. Así que tenía curiosidad por la seguridad / trampas.
An̲̳̳drew
1
Para un contenedor de propósito general para nuevo / eliminar, puede sobrecargar los operadores nuevo / eliminar. Dependiendo del entorno que use, probablemente obtenga enlaces a la administración de memoria para rastrear las asignaciones. Si termina en una situación en la que no sabe lo que está eliminando, tómelo como un fuerte indicio de que su diseño no es óptimo y necesita refactorización.
Hans
38
Creo que se cuestiona demasiado la pregunta en lugar de responderla. (No solo aquí, sino en todos los SO)
Petruza

Respuestas:

24

Depende de "seguro". Por lo general, funcionará porque la información se almacena junto con el puntero sobre la asignación en sí, por lo que el desasignador puede devolverla al lugar correcto. En este sentido, es "seguro" siempre que su asignador utilice etiquetas de límites internas. (Muchos hacen.)

Sin embargo, como se mencionó en otras respuestas, eliminar un puntero vacío no llamará a los destructores, lo que puede ser un problema. En ese sentido, no es "seguro".

No hay una buena razón para hacer lo que está haciendo de la forma en que lo está haciendo. Si desea escribir sus propias funciones de desasignación, puede usar plantillas de funciones para generar funciones con el tipo correcto. Una buena razón para hacerlo es generar asignadores de grupos, que pueden ser extremadamente eficientes para tipos específicos.

Como se mencionó en otras respuestas, este es un comportamiento indefinido en C ++. En general es bueno evitar comportamientos indefinidos, aunque el tema en sí es complejo y está lleno de opiniones encontradas.

Christopher
fuente
9
¿Cómo es esta una respuesta aceptada? No tiene ningún sentido "eliminar un puntero vacío": la seguridad es un punto discutible.
Kerrek SB
6
"No hay una buena razón para hacer lo que está haciendo de la forma en que lo está haciendo". Esa es tu opinión, no un hecho.
rxantos
8
@rxantos Proporciona un contraejemplo en el que hacer lo que el autor de la pregunta quiere hacer es una buena idea en C ++.
Christopher
Creo que esta respuesta es bastante razonable, pero también creo que cualquier respuesta a esta pregunta debe al menos mencionar que se trata de un comportamiento indefinido.
Kyle Strand
@Christopher Intente escribir un único sistema recolector de basura que no sea específico de tipo pero simplemente funcione. El hecho de que sizeof(T*) == sizeof(U*)para todos T,Usugiere que debería ser posible tener 1 void *implementación de recolector de basura basada en plantillas . Pero luego, cuando el gc realmente tiene que eliminar / liberar un puntero, surge exactamente esta pregunta. Para que funcione, o necesita envoltorios destructores de función lambda (urgh) o necesitaría algún tipo de "tipo como datos" dinámico que permita ir y venir entre un tipo y algo almacenable.
BitTickler
147

La eliminación mediante un puntero vacío no está definida por el estándar C ++; consulte la sección 5.3.5 / 3:

En la primera alternativa (eliminar objeto), si el tipo estático del operando es diferente de su tipo dinámico, el tipo estático será una clase base del tipo dinámico del operando y el tipo estático tendrá 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.

Y su nota al pie:

Esto implica que un objeto no se puede eliminar usando un puntero de tipo void * porque no hay objetos de tipo void

.


fuente
6
¿Estás seguro de haber acertado con la cotización? Creo que la nota al pie se refiere a este texto: "En la primera alternativa (eliminar objeto), si el tipo estático del operando es diferente de su tipo dinámico, el tipo estático será una clase base del tipo dinámico del operando y el tipo estático type debe tener un destructor virtual o el comportamiento es indefinido. En la segunda alternativa (eliminar matriz) si el tipo dinámico del objeto a eliminar difiere de su tipo estático, el comportamiento es indefinido. " :)
Johannes Schaub - litb
2
Tienes razón, he actualizado la respuesta. Sin embargo, no creo que niegue el punto básico.
No claro que no. Todavía dice que es UB. Aún más, ahora establece que de manera normativa que eliminar void * es UB :)
Johannes Schaub - litb
¿Llenar la memoria de dirección puntiaguda de un puntero vacío con NULLalguna diferencia en la gestión de la memoria de la aplicación?
artu-hnrq
25

No es una buena idea y no es algo que haría en C ++. Estás perdiendo la información de tu tipo sin ningún motivo.

No se llamará a su destructor en los objetos en su matriz que está eliminando cuando lo llame para tipos no primitivos.

En su lugar, debe anular nuevo / eliminar.

Eliminar el vacío * probablemente liberará su memoria correctamente por casualidad, pero está mal porque los resultados no están definidos.

Si por alguna razón desconocida para mí necesita almacenar su puntero en un vacío * y luego libérelo, debe usar malloc y free.

Brian R. Bondy
fuente
5
Tiene razón en que no se llama al destructor, pero se equivoca en que se desconoce el tamaño. Si le das a eliminar un puntero que recibió de nuevo, que no de hecho saber el tamaño de la cosa se va a eliminar, completamente aparte del tipo. Cómo lo hace no está especificado por el estándar C ++, pero he visto implementaciones donde el tamaño se almacena inmediatamente antes de los datos a los que apunta el puntero devuelto por 'nuevo'.
KeyserSoze
Se eliminó la parte sobre el tamaño, aunque el estándar C ++ dice que no está definido. Sé que malloc / free funcionaría para los punteros void *.
Brian R. Bondy
¿No cree que tiene un enlace web a la sección relevante del estándar? Sé que las pocas implementaciones de nuevo / eliminar que he analizado definitivamente funcionan correctamente sin conocimiento de tipos, pero admito que no he mirado lo que especifica el estándar. IIRC C ++ originalmente requería un recuento de elementos de matriz al eliminar matrices, pero ya no lo hace con las versiones más recientes.
KeyserSoze
Consulte la respuesta de @Neil Butterworth. Su respuesta debería ser la aceptada en mi opinión.
Brian R. Bondy
2
@keysersoze: Generalmente no estoy de acuerdo con tu declaración. El hecho de que alguna implementación haya almacenado el tamaño antes de la memoria asignada no significa que sea una regla.
13

Eliminar un puntero vacío es peligroso porque no se llamará a los destructores en el valor al que realmente apunta. Esto puede resultar en pérdidas de memoria / recursos en su aplicación.

JaredPar
fuente
1
char no tiene un constructor / destructor.
rxantos
9

La pregunta no tiene sentido. Su confusión puede deberse en parte al lenguaje descuidado que la gente usa con frecuencia delete:

Usas deletepara destruir un objeto que fue asignado dinámicamente. Hazlo, forma una expresión de eliminación con un puntero a ese objeto . Nunca "borras un puntero". Lo que realmente hace es "eliminar un objeto identificado por su dirección".

Ahora vemos por qué la pregunta no tiene sentido: un puntero vacío no es la "dirección de un objeto". Es solo una dirección, sin semántica. Se puede haber venido de la dirección de un objeto real, pero se pierde esa información, ya que fue codificada en el tipo del puntero originales. La única forma de restaurar un puntero de objeto es devolver el puntero vacío a un puntero de objeto (lo que requiere que el autor sepa lo que significa el puntero). voiden sí mismo es un tipo incompleto y, por lo tanto, nunca el tipo de un objeto, y un puntero vacío nunca puede usarse para identificar un objeto. (Los objetos se identifican conjuntamente por su tipo y su dirección).

Kerrek SB
fuente
Es cierto que la pregunta no tiene mucho sentido sin ningún contexto circundante. Algunos compiladores de C ++ aún compilarán felizmente este tipo de código sin sentido (si se sienten útiles, pueden ladrar una advertencia al respecto). Entonces, se hizo la pregunta para evaluar los riesgos conocidos de ejecutar código heredado que contiene esta operación desacertada: ¿fallará? ¿pierde parte o toda la memoria de matriz de caracteres? ¿Algo más específico de la plataforma?
An̲̳̳drew
Gracias por la atenta respuesta. ¡Voto a favor!
An̲̳̳drew
1
@Andrew: Me temo que el estándar es bastante claro en esto: "El valor del operando de deletepuede ser un valor de puntero nulo, un puntero a un objeto que no es una matriz creado por una nueva expresión anterior o un puntero a un subobjeto que representa una clase base de dicho objeto. De lo contrario, el comportamiento no está definido ". Entonces, si un compilador acepta su código sin un diagnóstico, no es más que un error en el compilador ...
Kerrek SB
1
@KerrekSB - No es más que un error en el compilador - No estoy de acuerdo. El estándar dice que el comportamiento no está definido. Esto significa que el compilador / implementación puede hacer cualquier cosa y aún así cumplir con el estándar. Si la respuesta del compilador es decir que no puede eliminar un puntero void *, está bien. Si la respuesta del compilador es borrar el disco duro, también está bien. OTOH, si la respuesta del compilador es no generar ningún diagnóstico, sino generar código que libere la memoria asociada con ese puntero, también está bien. Esta es una forma sencilla de lidiar con esta forma de UB.
David Hammen
Solo para agregar: no estoy tolerando el uso de delete void_pointer. Es un comportamiento indefinido. Los programadores nunca deben invocar un comportamiento indefinido, incluso si la respuesta parece hacer lo que el programador quería que se hiciera.
David Hammen
6

Si realmente tiene que hacer esto, ¿por qué no cortar al hombre medio (las newy deletelos operadores) y llamar a lo global operator newy operator deletedirecta? (Por supuesto, si está tratando de instrumentar los operadores newy delete, en realidad debería volver a implementar operator newy operator delete).

void* my_alloc (size_t size)
{
   return ::operator new(size);
}

void my_free (void* ptr)
{
   ::operator delete(ptr);
}

Tenga en cuenta que malloc(), a diferencia de , operator newarroja std::bad_allocfallos (o llama al new_handlersi hay uno registrado).

bk1e
fuente
Esto es correcto, ya que char no tiene un constructor / destructor.
rxantos
5

Porque char no tiene una lógica destructora especial. ESTO no funcionará.

class foo
{
   ~foo() { printf("huzza"); }
}

main()
{
   foo * myFoo = new foo();
   delete ((void*)foo);
}

No llamarán al d'ctor.


fuente
5

Si desea usar void *, ¿por qué no usa solo malloc / free? new / delete es más que solo administrar la memoria. Básicamente, new / delete llama a un constructor / destructor y están sucediendo más cosas. Si solo usa tipos integrados (como char *) y los elimina a través de void *, funcionaría, pero aún así no se recomienda. La conclusión es usar malloc / free si desea usar void *. De lo contrario, puede utilizar funciones de plantilla para su conveniencia.

template<typename T>
T* my_alloc (size_t size)
{
   return new T [size];
}

template<typename T>
void my_free (T* ptr)
{
   delete [] ptr;
}

int main(void)
{
    char* pChar = my_alloc<char>(10);
    my_free(pChar);
}
joven
fuente
No escribí el código en el ejemplo; me topé con este patrón que se usaba en un par de lugares, mezclando curiosamente la administración de memoria C / C ++, y me preguntaba cuáles eran los peligros específicos.
An̲̳̳drew
Escribir C / C ++ es una receta para el fracaso. Quien haya escrito eso debería haber escrito uno o el otro.
David Thornley
2
@David Eso es C ++, no C / C ++. C no tiene plantillas, ni usa new and delete.
rxantos
5

Mucha gente ya ha comentado diciendo que no, no es seguro eliminar un puntero vacío. Estoy de acuerdo con eso, pero también quería agregar que si está trabajando con punteros vacíos para asignar matrices contiguas o algo similar, puede hacer esto newpara que pueda usarlo de deletemanera segura (con, ejem , un poco de trabajo extra). Esto se hace asignando un puntero vacío a la región de memoria (llamado 'arena') y luego suministrando el puntero a la arena a new. Consulte esta sección en las preguntas frecuentes de C ++ . Este es un enfoque común para implementar grupos de memoria en C ++.

Paul Morie
fuente
0

Difícilmente hay una razón para hacer esto.

En primer lugar, si no conoce el tipo de datos, y todo lo que sabe es que lo es void*, entonces realmente debería tratar esos datos como un blob sin tipo de datos binarios ( unsigned char*) y usar malloc/ freepara tratarlos. . Esto es necesario a veces para cosas como datos de forma de onda y similares, donde necesita pasarvoid* punteros a C apis. Esta bien.

Si lo hace saber el tipo de los datos (es decir, tiene una ctor / dtor), pero por alguna razón que terminó con un void*puntero (por cualquier razón que tenga) entonces usted realmente debería echarlo hacia atrás con el tipo que te des cuenta de sé , y llámalo delete.

bobobobo
fuente
0

He usado void *, (también conocido como tipos desconocidos) en mi marco durante la reflexión de código y otras hazañas de ambigüedad, y hasta ahora, no he tenido problemas (pérdida de memoria, violaciones de acceso, etc.) de ningún compilador. Solo advertencias debido a que el funcionamiento no es estándar.

Tiene mucho sentido eliminar un desconocido (vacío *). Solo asegúrese de que el puntero siga estas pautas, o puede dejar de tener sentido:

1) El puntero desconocido no debe apuntar a un tipo que tenga un deconstructor trivial, por lo que cuando se lanza como puntero desconocido NUNCA SE DEBE BORRAR. Solo elimine el puntero desconocido DESPUÉS de convertirlo de nuevo en el tipo ORIGINAL.

2) ¿Se hace referencia a la instancia como un puntero desconocido en la memoria enlazada a la pila o al montón? Si el puntero desconocido hace referencia a una instancia en la pila, ¡NUNCA SE DEBE BORRAR!

3) ¿Está 100% seguro de que el puntero desconocido es una región de memoria válida? No, ¡NUNCA SE DEBE BORRAR!

En total, hay muy poco trabajo directo que se pueda hacer usando un tipo de puntero desconocido (vacío *). Sin embargo, indirectamente, el vacío * es un gran activo en el que los desarrolladores de C ++ pueden confiar cuando se requiere ambigüedad de datos.

zackery.fix
fuente
0

Si solo desea un búfer, use malloc / free. Si debe usar new / delete, considere una clase contenedora trivial:

template<int size_ > struct size_buffer { 
  char data_[ size_]; 
  operator void*() { return (void*)&data_; }
};

typedef sized_buffer<100> OpaqueBuffer; // logical description of your sized buffer

OpaqueBuffer* ptr = new OpaqueBuffer();

delete ptr;
Sanjaya R
fuente
0

Para el caso particular de char.

char es un tipo intrínseco que no tiene un destructor especial. Así que los argumentos de las filtraciones son discutibles.

sizeof (char) suele ser uno, por lo que tampoco hay un argumento de alineación. En el caso de una plataforma rara donde el tamaño de (char) no es uno, asignan memoria alineada lo suficiente para su char. Así que el argumento de la alineación también es discutible.

malloc / free sería más rápido en este caso. Pero pierde std :: bad_alloc y tiene que verificar el resultado de malloc. Llamar a los operadores globales nuevos y de eliminación podría ser mejor, ya que evita al intermediario.

rxantos
fuente
1
" sizeof (char) suele ser uno " sizeof (char) SIEMPRE es uno
curioso
No es hasta hace poco (2019) que la gente piensa que en newrealidad está definido lanzar. Esto no es verdad. Depende del compilador y del modificador del compilador. Consulte, por ejemplo, /GX[-] enable C++ EH (same as /EHsc)conmutadores MSVC2019 . También en los sistemas integrados, muchos eligen no pagar los impuestos sobre el rendimiento de las excepciones de C ++. Entonces, la oración que comienza con "Pero pierdes std :: bad_alloc ..." es cuestionable.
BitTickler