Considere el siguiente código:
#include <iostream>
struct foo
{
// (a):
void bar() { std::cout << "gman was here" << std::endl; }
// (b):
void baz() { x = 5; }
int x;
};
int main()
{
foo* f = 0;
f->bar(); // (a)
f->baz(); // (b)
}
Esperamos (b)
fallar, porque no hay un miembro correspondiente x
para el puntero nulo. En la práctica, (a)
no se bloquea porque this
nunca se usa el puntero.
Debido a que (b)
elimina la referencia al this
puntero ( (*this).x = 5;
), y this
es nulo, el programa entra en un comportamiento indefinido, ya que la eliminación de referencias nulas siempre se dice que es un comportamiento indefinido.
¿Tiene como (a)
resultado un comportamiento indefinido? ¿Qué pasa si ambas funciones (y x
) son estáticas?
x
se hizo estática. :)Respuestas:
Ambos
(a)
y(b)
dan como resultado un comportamiento indefinido. Siempre es un comportamiento indefinido llamar a una función miembro a través de un puntero nulo. Si la función es estática, técnicamente tampoco está definida, pero hay alguna disputa.Lo primero que hay que entender es por qué es un comportamiento indefinido desreferenciar un puntero nulo. En C ++ 03, en realidad hay un poco de ambigüedad aquí.
Aunque "desreferenciar un puntero nulo da como resultado un comportamiento indefinido" se menciona en notas tanto en §1.9 / 4 como en §8.3.2 / 4, nunca se indica explícitamente. (Las notas no son normativas).
Sin embargo, se puede intentar deducirlo de §3.10 / 2:
Al eliminar la referencia, el resultado es un valor l. Un puntero nulo no se refiere a un objeto, por lo tanto, cuando usamos lvalue tenemos un comportamiento indefinido. El problema es que la oración anterior nunca se dice, entonces, ¿qué significa "usar" el valor l? ¿Solo generarlo o usarlo en el sentido más formal de realizar conversión de valor a valor?
Independientemente, definitivamente no se puede convertir a un rvalue (§4.1 / 1):
Aquí es definitivamente un comportamiento indefinido.
La ambigüedad proviene de si es o no un comportamiento indefinido para deferencia pero no usar el valor de un puntero no válido (es decir, obtener un lvalue pero no convertirlo en un rvalue). Si no, entonces
int *i = 0; *i; &(*i);
está bien definido. Este es un tema activo .Así que tenemos una vista estricta de "desreferenciar un puntero nulo, obtener un comportamiento indefinido" y una vista débil de "usar un puntero nulo desreferenciado, obtener un comportamiento indefinido".
Ahora consideramos la pregunta.
Sí,
(a)
da como resultado un comportamiento indefinido. De hecho, sithis
es nulo, independientemente del contenido de la función, el resultado no está definido.Esto se deriva de §5.2.5 / 3:
*(E1)
dará como resultado un comportamiento indefinido con una interpretación estricta y lo.E2
convierte en un valor r, lo que lo convierte en un comportamiento indefinido para la interpretación débil.También se deduce que es un comportamiento indefinido directamente de (§9.3.1 / 1):
Con funciones estáticas, la interpretación estricta frente a la débil marca la diferencia. Estrictamente hablando, no está definido:
Es decir, se evalúa como si no fuera estático y una vez más desreferenciamos un puntero nulo con
(*(E1)).E2
.Sin embargo, debido a
E1
que no se usa en una llamada de función miembro estática, si usamos la interpretación débil, la llamada está bien definida.*(E1)
da como resultado un valor l, la función estática se resuelve,*(E1)
se descarta y se llama a la función. No hay conversión de lvalue a rvalue, por lo que no hay un comportamiento indefinido.En C ++ 0x, a partir de n3126, la ambigüedad permanece. Por ahora, tenga cuidado: use la interpretación estricta.
fuente
*p
no es un error cuandop
es nulo a menos que el lvalue se convierta en un rvalue". Sin embargo, eso se basa en el concepto de un "valor l vacío", que es parte de la resolución propuesta al defecto 232 del CWG , pero que no ha sido adoptado. Entonces, con el lenguaje tanto en C ++ 03 como en C ++ 0x, desreferenciar el puntero nulo aún no está definido, incluso si no hay conversión de lvalue a rvalue.p
fuera una dirección de hardware que desencadenara alguna acción cuando se leyera, pero no se declararavolatile
, la declaración*p;
no sería necesaria, pero estaría permitida , para leer esa dirección; la declaración&(*p);
, sin embargo, tendría prohibido hacerlo. Si lo*p
fueravolatile
, se requeriría la lectura. En cualquier caso, si el puntero no es válido, no puedo ver cómo la primera declaración no sería Comportamiento indefinido, pero tampoco puedo ver por qué lo sería la segunda declaración.Obviamente indefinido significa que es no está definido , pero a veces puede ser predecible. Nunca se debe confiar en la información que estoy a punto de proporcionar para el código de trabajo, ya que ciertamente no está garantizada, pero podría resultar útil al depurar.
Podría pensar que llamar a una función en un puntero de objeto eliminará la referencia del puntero y provocará UB. En la práctica, si la función no es virtual, el compilador la habrá convertido en una llamada de función simple pasando el puntero como primer parámetro. esto , omitiendo la desreferencia y creando una bomba de tiempo para la función miembro llamada. Si la función miembro no hace referencia a ninguna variable miembro o función virtual, en realidad podría tener éxito sin errores. ¡Recuerde que el éxito cae dentro del universo de "indefinido"!
Función MFC de Microsoft GetSafeHwnd realidad se basa en este comportamiento. No sé qué estaban fumando.
Si está llamando a una función virtual, el puntero debe ser desreferenciado para llegar a la vtable, y seguro que obtendrá UB (probablemente un bloqueo, pero recuerde que no hay garantías).
fuente
GetSafeHwnd
, es posible que lo hayan mejorado desde entonces. ¡Y no olvide que tienen conocimiento interno sobre el funcionamiento del compilador!