¿Cuándo la invocación de una función miembro en una instancia nula da como resultado un comportamiento indefinido?

120

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 xpara el puntero nulo. En la práctica, (a)no se bloquea porque thisnunca se usa el puntero.

Debido a que (b)elimina la referencia al thispuntero ( (*this).x = 5;), y thises 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?

GManNickG
fuente
Si ambas funciones son estáticas , ¿cómo podría referirse x dentro de baz ? (x es una variable miembro no estática)
legends2k
4
@ legends2k: La simulación también xse hizo estática. :)
GManNickG
Seguramente, pero para el caso (a) funciona igual en todos los casos, es decir, se invoca la función. Sin embargo, al reemplazar el valor del puntero de 0 a 1 (por ejemplo, a través de reinterpret_cast), casi invariablemente siempre falla. ¿La asignación de valor de 0 y por tanto NULL, como en el caso a, representa algo especial para el compilador? ¿Por qué siempre choca con cualquier otro valor que se le asigne?
Siddharth Shankaran
5
Interesante: en la próxima revisión de C ++, no habrá más desreferenciación de punteros. Ahora realizaremos indirección a través de punteros. Para obtener más información, realice una indirección a través de este enlace: N3362
James McNellis
3
Invocar una función miembro en un puntero nulo es siempre un comportamiento indefinido. ¡Con solo mirar su código, ya puedo sentir el comportamiento indefinido subiendo lentamente por mi cuello!
fredoverflow

Respuestas:

113

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:

Un lvalue se refiere a un objeto o función.

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):

Si el objeto al que se refiere lvalue 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 necesita 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 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, si thises nulo, independientemente del contenido de la función, el resultado no está definido.

Esto se deriva de §5.2.5 / 3:

Si E1tiene el tipo "puntero a la clase X", la expresión E1->E2se convierte a la forma equivalente(*(E1)).E2;

*(E1)dará como resultado un comportamiento indefinido con una interpretación estricta y lo .E2convierte 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):

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 de miembro de clase, en cuyo caso se evalúa la expresión de objeto.

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 E1que 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.

GManNickG
fuente
5
+1. Continuando con la pedantería, bajo la "definición débil" la función miembro no estática no ha sido llamada "para un objeto que no es de tipo X". Se ha llamado para un valor l que no es un objeto en absoluto. Entonces, la solución propuesta agrega el texto "o si el lvalue es un lvalue vacío" a la cláusula que cita.
Steve Jessop
¿Podrías aclarar un poco? En particular, con los enlaces de "problema cerrado" y "problema activo", ¿cuáles son los números de los problemas? Además, si se trata de un problema cerrado, ¿cuál es exactamente la respuesta sí / no para las funciones estáticas? Siento que me estoy perdiendo el paso final para tratar de entender tu respuesta.
Brooks Moses
4
No creo que el defecto 315 de CWG sea tan "cerrado" como implica su presencia en la página de "problemas cerrados". La justificación dice que debería permitirse porque " *pno es un error cuando pes 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.
James McNellis
1
@JamesMcNellis: Según tengo entendido, si pfuera una dirección de hardware que desencadenara alguna acción cuando se leyera, pero no se declarara volatile, 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 *pfuera volatile, 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.
supercat
1
".E2 lo convierte en un rvalue", - Uh, no, no lo hace
MM
30

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).

Mark Ransom
fuente
1
GetSafeHwnd primero hace una! Esta comprobación y, si es verdadero, devuelve NULL. Luego comienza un marco SEH y elimina la referencia del puntero. si hay una infracción de acceso a la memoria (0xc0000005), se detecta y se devuelve NULL a la persona que llama :) De lo contrario, se devuelve el HWND.
Петър Петров
@ ПетърПетров han pasado bastantes años desde que miré el código GetSafeHwnd, es posible que lo hayan mejorado desde entonces. ¡Y no olvide que tienen conocimiento interno sobre el funcionamiento del compilador!
Mark Ransom
Estoy indicando una posible implementación de muestra que tiene el mismo efecto, lo que realmente hace es realizar ingeniería inversa con un depurador :)
Петър Петров
1
"¡Tienen conocimiento interno sobre el funcionamiento del compilador!" - la causa de problemas eternos para proyectos como MinGW que intentan permitir que g ++ compile código que llama a la API de Windows
MM
@MM Creo que todos estaríamos de acuerdo en que esto es injusto. Y debido a eso, también creo que hay una ley sobre compatibilidad que hace que sea un poco ilegal mantenerlo así.
v.oddou