c++ - que - como eliminar un mensaje enviado por messenger
¿Se permite eliminar para modificar su parámetro? (5)
La indicación a través de un valor de puntero no válido y la transferencia de un valor de puntero no válido a una función de desasignación tienen un comportamiento indefinido. Cualquier otro uso de un valor de puntero no válido tiene un comportamiento definido por la implementación.
Entonces, después de
delete ptr;
, el valor deptr
convierte en un valor de puntero no válido y el uso de este valor tiene un comportamiento definido por la implementación.
La norma dice que el valor del puntero pasado "se vuelve inválido", es decir, su estado ha cambiado, por lo que ciertas llamadas se vuelven indefinidas y la implementación puede tratarlo de manera diferente.
El lenguaje no es muy claro, pero aquí está el contexto:
6.7 duración de almacenamiento
4 Cuando se alcanza el final de la duración de una región de almacenamiento, los valores de todos los punteros que representan la dirección de cualquier parte de esa región de almacenamiento se convierten en valores de puntero no válidos (6.9.2). La indicación a través de un valor de puntero no válido y la transferencia de un valor de puntero no válido a una función de desasignación tienen un comportamiento indefinido. Cualquier otro uso de un valor de puntero no válido tiene un comportamiento definido por la implementación.
6.9.2 Tipos de compuestos
Cada valor del tipo de puntero es uno de los siguientes:
(3.1) - un puntero a un objeto o función (se dice que el puntero apunta al objeto o función), o
(3.2) - un puntero más allá del final de un objeto (8.7), o
(3.3) - el valor del puntero nulo (7.11) para ese tipo, o
(3.4) - un valor de puntero no válido.
Son los valores del puntero de tipo que son o no son inválidos, y se "vuelven" así de acuerdo con el progreso de la ejecución de un programa en la máquina abstracta de C ++.
El estándar no habla de cambios en el valor que tiene la variable / objeto direccionado por un valor l o de la asociación de un símbolo con un valor.
C ++ permite explícitamente una implementación de eliminar para poner a cero un operando lvalue, y esperaba que las implementaciones hicieran eso, pero esa idea no parece haberse vuelto popular entre los implementadores.
Por separado de esto, Stroustrup dice que si una expresión de operando era un valor l modificable, es decir, una expresión de operando era la dirección de una variable / objeto que tiene un valor de puntero que pasa, después de lo cual el estado de ese valor es "inválido", entonces la implementación puede establecer el valor mantenido por esa variable / objeto a cero.
Sin embargo, no dice que se
ptr
que el valor deptr
cambie.
Stroustrup es informal al hablar sobre lo que puede hacer una implementación. El estándar define cómo una máquina abstracta de C ++ puede / no puede / puede comportarse. Aquí Stroustrup está hablando de una implementación hipotética que se parece a esa máquina. El "valor de ptr
" se "permite cambiar" porque el comportamiento definido e indefinido no le permite saber cuál es ese valor en el momento de la desasignación, y el comportamiento definido por la implementación puede ser cualquier cosa, por lo que puede ser que el variable / objeto tiene un valor diferente.
No tiene sentido hablar sobre un cambio de valor. No puede "poner a cero" un valor; puede poner a cero una variable / objeto, y eso es lo que queremos decir cuando decimos "cero fuera" un valor l - ponga a cero la variable / objeto al que hace referencia / identifica. Incluso si estira "cero a cero" para incluir la asociación de un nuevo valor con un nombre o literal, la implementación puede hacer esto, porque no puede "usar" el valor en tiempo de ejecución a través del nombre o literal para averiguar si todavía está asociado con el mismo valor
(Sin embargo, dado que todo lo que uno puede hacer con un valor es "usarlo" en un programa pasando un valor l que identifica una variable / objeto que lo sostiene a un operador o pasando una referencia o constante que lo denota a un operador, y un operador puede actuar como si se hubiera pasado un valor diferente , supongo que podría razonablemente informalmente capturarlo descuidadamente como "valores que cambian de valor" en la implementación).
Si el contenido de esa matriz se copia de nuevo en el objeto, el objeto conservará posteriormente su valor original.
Pero copiarlo lo está usando, por lo que copiarlo está definido por la implementación una vez que es "inválido". Entonces, llamar a un programa que normalmente lo copiaría está definido por la implementación. Esto se pone de manifiesto en la nota de pie de página que da el ejemplo de que
Algunas implementaciones pueden definir que la copia de un valor de puntero no válido provoque un error de tiempo de ejecución generado por el sistema
Nada hace lo que normalmente hace a partir de un comportamiento indefinido / definido por la implementación. Usamos el comportamiento normal para determinar una secuencia de cambios en una máquina abstracta, y si surge un cambio de estado definido por la implementación, entonces las cosas actúan como si la implementación las definiera para actuar, no como lo hacen normalmente. Lamentablemente, el significado de "uso de" un valor no está claro. No sé por qué crees que 6.9 "garantiza" algo más o menos que cualquier otro elemento de memoria, que después de un estado indefinido / definido por la implementación no es nada.
En una respuesta, https://stackoverflow.com/a/704568/8157187 , hay una cita de Stroustrup:
C ++ permite explícitamente una implementación de eliminar para poner a cero un operando lvalue, y esperaba que las implementaciones hicieran eso, pero esa idea no parece haberse vuelto popular entre los implementadores.
Sin embargo, no pude encontrar esta declaración explícita en el estándar. Hay una parte del borrador del estándar actual (N4659), que se puede interpretar de esta manera:
6.7:
Cuando se alcanza el final de la duración de una región de almacenamiento, los valores de todos los punteros que representan la dirección de cualquier parte de esa región de almacenamiento se convierten en valores de puntero no válidos (6.9.2). La indicación a través de un valor de puntero no válido y la transferencia de un valor de puntero no válido a una función de desasignación tienen un comportamiento indefinido. Cualquier otro uso de un valor de puntero no válido tiene un comportamiento definido por la implementación.
Nota: Algunas implementaciones pueden definir que la copia de un valor de puntero no válido provoque un error de tiempo de ejecución generado por el sistema
Entonces, después de delete ptr;
, el valor de ptr
convierte en un valor de puntero no válido y el uso de este valor tiene un comportamiento definido por la implementación. Sin embargo, no dice que se ptr
que el valor de ptr
cambie.
Podría ser una pregunta filosófica, ¿cómo puede uno decidir que un valor ha cambiado, si uno no puede usar su valor?
6.9:
Para cualquier objeto (que no sea un subobjeto de clase base) de tipo T trivialmente copiable, independientemente de que el objeto tenga o no un valor válido de tipo T, los bytes subyacentes (4.4) que componen el objeto se pueden copiar en una matriz de caracteres, unsigned char, o std :: byte (21.2.1) .43 Si el contenido de esa matriz se copia de nuevo en el objeto, el objeto conservará posteriormente su valor original.
Por lo tanto, parece que es válido para memcpy
un valor de puntero no válido en una matriz de caracteres (dependiendo de qué enunciado es "más fuerte", 6.7 o 6.9. Para mí, 6.9 parece más fuerte).
De esta forma, puedo detectar que el valor del puntero ha sido cambiado por delete
: memcpy
el valor del puntero antes y después de la matriz delete
to char, luego los comparo.
Entonces, según tengo entendido, 6.7 no garantiza que la delete
permita modificar su parámetro.
¿Se permite eliminar para modificar su parámetro?
Mira los comentarios aquí: https://stackoverflow.com/a/45142972/8157187
Aquí hay un código del mundo real poco probable, pero aún posible, donde esto es importante:
SomeObject *o = ...; // We have a SomeObject
// This SomeObject is registered into someHashtable, with its memory address
// The hashtable interface is C-like, it handles opaque keys (variable length unsigned char arrays)
delete o;
unsigned char key[sizeof(o)];
memcpy(key, &o, sizeof(o)); // Is this line OK? Is its behavior implementation defined?
someHashtable.remove(key, sizeof(key)); // Remove o from the hashtable
Por supuesto, este fragmento se puede reordenar, por lo que se convierte en un código seguramente válido. Pero la pregunta es: ¿es este un código válido?
Aquí hay una línea de pensamiento relacionada: supongamos que una implementación define qué describe la nota al pie:
copiar un valor de puntero no válido provoca un error en el tiempo de ejecución generado por el sistema
6.9 garantiza que puedo memcpy()
cualquier valor. Incluso uno inválido. Entonces, en esta implementación teórica, cuando memcpy()
el valor del puntero no válido (que debería tener éxito, 6.9 lo garantiza), en cierto sentido, no utilizo el valor del puntero no válido, sino solo sus bytes subyacentes (porque generaría un falla en tiempo de ejecución, y 6.9 no lo permite), por lo que 6.7 no se aplica .
Antes de la eliminación, el valor de ptr
era válido. Después de la eliminación, el valor no es válido. Por lo tanto, el valor cambió. Los valores válidos y los valores no válidos son mutuamente excluyentes; un valor no puede ser simultáneamente válido e inválido.
Tu pregunta tiene un concepto erróneo básico; estás combinando estos dos conceptos diferentes:
- El valor de una variable
- La representación de una variable en la memoria.
No hay correspondencia uno-a-uno entre estas dos cosas. El mismo valor puede tener representaciones múltiples, y la misma representación puede corresponder a diferentes valores.
Creo que la esencia de su pregunta es: puede delete ptr;
cambiar la representación de ptr
? . A lo que la respuesta es "Sí". Podría insertar el puntero eliminado en una matriz char, inspeccionar los bytes y encontrarlos como bytes de valor cero (o cualquier otra cosa). Esto está cubierto en el estándar por C ++ 14 [basic.stc.dynamic.deallocation] / 4 (o C ++ 17 [basic.stc] / 4):
Cualquier otro uso de un valor de puntero no válido tiene un comportamiento definido por la implementación.
Está definido por la implementación y la implementación podría definir que inspeccionar los bytes da bytes con valor cero.
El fragmento de código se basa en el comportamiento definido por la implementación. El "código válido" no es la terminología utilizada por el estándar, pero es posible que el código no elimine el elemento deseado de la tabla hash.
Como lo aludió Stroustrup, esta es una decisión de diseño intencional. Un ejemplo de uso sería un compilador en modo de depuración que establece los punteros borrados en una representación particular, de modo que puede generar un error de tiempo de ejecución si se utiliza posteriormente un puntero eliminado. Aquí hay un ejemplo de ese principio en acción para punteros no inicializados.
Nota histórica: en C ++ 11 este caso no estaba definido , en lugar de definición de implementación. Entonces el comportamiento de usar un puntero borrado era idéntico al comportamiento de usar un puntero no inicializado. En el lenguaje C, liberar memoria se define como poner todos los punteros a esa memoria en el mismo estado que tiene un puntero no inicializado.
El contexto en el que encontró la declaración de Stroustrup, está disponible bajo Stroustrup, borre el cero
Stroustrup te deja considerar
delete p;
// ...
delete p;
Después de la primera eliminación, el puntero p no era válido. La segunda eliminación es incorrecta, pero no tendría efecto si p se configurara en 0 después de la primera eliminación.
La idea de Stroustrups fue compilarlo como algo así
delete p; p = 0;
// ...
delete p;
eliminarse a sí mismo no puede poner a cero el puntero dado que pasó void *
pero no está void *&
Sin embargo, encuentro cero fuera p no ayuda mucho ya que pueden existir otras copias de ese puntero que también pueden borrarse accidentalmente. Una mejor forma es usar los diferentes tipos de smartpointers.
Especificación 6.7
Dice que cualquier puntero con la dirección del puntero p no será válido (en una forma de implementación definida) después de una eliminación p. No dice nada sobre cambiar la dirección del puntero, ni está permitido ni está prohibido.
Spec 6.9
El prerrequisito de 6.9 es un objeto (válido o no). Esta especificación no se aplica aquí ya que p (la dirección) no es más válida después de eliminar y, por lo tanto, NO apunta a un objeto. Entonces no hay contradicción, y cualquier discusión sobre si 6.7 o 6.9 es más fuerte no es válida.
La especificación también requiere copiar los bytes a la ubicación original del objeto, que su código no tiene, y tampoco podría, porque el objeto original ha sido eliminado.
Sin embargo, no veo ninguna razón para empacar una dirección de puntero en una matriz de caracteres y pasarla. Y los punteros siempre tienen el mismo tamaño en una implementación determinada. Tu código es solo una versión engorrosa de:
SomeObject *o = ...;
void *orgO = o;
delete o;
someHashtable.remove(orgO);
// someHashtable.remove(o); o might be set to 0
Sin embargo, este código todavía se ve extraño. Para obtener un objeto de la tabla hash, necesita el puntero a ese objeto. ¿Por qué no usar directamente el puntero directamente?
Una tabla hash debería ayudar a encontrar objetos por algunos valores invariables de los objetos. Esa no es su aplicación de tabla hash
¿Intentó tener una lista de todas las instancias válidas de SomeObject
?
Su código es una causa no válida , de acuerdo con Stroustrup, el compilador puede establecer p en cero. Si esto sucedió, tu código se bloqueará
Para que una función de eliminación actualice el puntero, necesita conocer la dirección de este puntero y también hacer la actualización. Esto requeriría memoria extra, pocas operaciones adicionales y compatibilidad con el compilador. Parece más o menos trivial en tu ejemplo.
Ahora imagina una cadena de funciones que pasan el puntero el uno al otro en argumentos y solo el último realmente eliminará. ¿Qué punteros actualizar en un caso así? ¿El último? ¿Todas? Para este último necesitaría crear una lista dinámica de punteros:
Objec *o = ...
handle(o);
void handle(Object *o){
if (deleteIt) doDelete(0);
else doSomethingElseAndThenPossiblyDeleteIt(o);
}
void doDelete(Object *o) {
delete o;
}
Por lo tanto, filosóficamente, si la eliminación pudiera modificar su parámetro, se abriría una lata de warms que reducirá la eficacia del programa. Por lo tanto, no está permitido y espero que nunca lo sea. El comportamiento indefinido es probablemente lo más natural en esos casos.
En cuanto a los contenidos de la memoria, lamentablemente vi demasiados errores en los que una memoria eliminada se sobrescribe después de que se eliminó el puntero. Y ... funciona bien hasta que llega un momento. Dado que la memoria está marcada como libre, puede ser reutilizada por otros objetos con el tiempo con consecuencias muy poco interesantes y mucha depuración. Entonces, filosóficamente de nuevo, c ++ no es un lenguaje fácil de programar. Hay otras herramientas que pueden detectar estos problemas, sin ningún soporte de idiomas.
delete
se define en [expr.delete] para invocar una función de desasignación, y las funciones de desasignación se definen en [basic.stc.dynamic.deallocation] como:
Cada función de desasignación volverá a ser
void
y su primer parámetro serávoid*
.
Dado que todas las funciones de desasignación tienen void*
, no un void*&
, no hay ningún mecanismo para que puedan modificar sus parámetros.