¿Cuándo la invocación de una función miembro a través de un puntero nulo da como resultado un comportamiento indefinido?
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 ningún miembro correspondiente x
para el puntero nulo. En la práctica, (a)
no falla porque el this
puntero nunca se usa.
Debido a que (b)
desreferencia el this
puntero ( (*this).x = 5;
) y this
es nulo, el programa ingresa en un comportamiento indefinido, ya que siempre se dice que desreferenciar nulo es un comportamiento indefinido.
¿Resulta (a)
en un comportamiento indefinido? ¿Qué pasa si ambas funciones (y x
) son estáticas?
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 existe cierta disputa.
Lo primero que hay que entender es por qué es un comportamiento indefinido eliminar la referencia a un puntero nulo. En C++03, en realidad hay un poco de ambigüedad aquí.
Aunque "eliminar la referencia a un puntero nulo da como resultado un comportamiento indefinido" se menciona en las notas de §1.9/4 y §8.3.2/4, nunca se indica explícitamente. (Las notas no son normativas).
Sin embargo, se puede intentar deducirlo del §3.10/2:
Un valor l se refiere a un objeto o función.
Al eliminar la referencia, el resultado es un valor l. Un puntero nulo no hace referencia a un objeto, por lo tanto, cuando usamos el valor l tenemos un comportamiento indefinido. El problema es que la oración anterior nunca se menciona, entonces, ¿qué significa "usar" el valor l? ¿Simplemente generarlo o usarlo en el sentido más formal de realizar una conversión de valor a valor?
De todos modos, definitivamente no se puede convertir a un valor r (§4.1/1):
Si el objeto al que se refiere el valor l no es un objeto de tipo T y no es un objeto de un tipo derivado de T, o si el objeto no está inicializado, un programa que requiere esta conversión tiene un comportamiento indefinido.
Aquí es definitivamente un comportamiento indefinido.
La ambigüedad proviene de si es o no un comportamiento indefinido deferencia pero no usar el valor de un puntero no válido (es decir, obtener un valor l pero no convertirlo en un valor r). 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 "usar un puntero nulo desreferenciado, obtener un comportamiento indefinido".
Ahora consideramos la pregunta.
Sí, (a)
da como resultado un comportamiento indefinido. De hecho, si this
es nulo, independientemente del contenido de la función, el resultado no está definido.
Esto se desprende del §5.2.5/3:
Si
E1
tiene el tipo "puntero a clase X", entonces la expresiónE1->E2
se convierte a la forma equivalente(*(E1)).E2;
*(E1)
dará como resultado un comportamiento indefinido con una interpretación estricta y .E2
lo convierte en un valor r, convirtiéndolo 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):
Si se llama a una función miembro no estática de una clase X para un objeto que no es de tipo X, o de un tipo derivado de X, el comportamiento no está definido.
Con funciones estáticas, la interpretación estricta frente a la débil marca la diferencia. Estrictamente hablando, no está definido:
Se puede hacer referencia a un miembro estático utilizando la sintaxis de acceso a miembros de clase, en cuyo caso se evalúa la expresión-objeto.
Es decir, se evalúa como si fuera no estático y una vez más desreferenciamos un puntero nulo con (*(E1)).E2
.
Sin embargo, debido a que E1
no se usa en una llamada a una 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 valor a valor, por lo que no hay un comportamiento indefinido.
En C++ 0x, a partir de n3126, la ambigüedad persiste. Por ahora, tenga cuidado: utilice la interpretación estricta.
Obviamente, indefinido significa que 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 puede resultar útil al depurar.
Se podría pensar que llamar a una función en un puntero de objeto eliminará la referencia al 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 this , evitando 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, es posible que se realice correctamente sin errores. ¡Recuerda que triunfar entra dentro del universo de lo "indefinido"!
La función MFC de Microsoft GetSafeHwnd en realidad se basa en este comportamiento. No sé qué estaban fumando.
Si está llamando a una función virtual, se debe desreferenciar el puntero para llegar a la vtable, y con seguridad obtendrá UB (probablemente un bloqueo, pero recuerde que no hay garantías).