que - c++ acceder a miembros estáticos usando un puntero nulo
variable automatica (4)
se ejecuta bien y produce el resultado esperado en lugar de cualquier error de tiempo de ejecución.
Eso es un error de suposición básica. Lo que está haciendo es un comportamiento indefinido , lo que significa que su reclamación por cualquier tipo de "salida esperada" es defectuosa.
Anexo: Tenga en cuenta que, si bien hay un informe de defecto de CWG ( #315 ) que está cerrado como "de acuerdo" de no realizar el UB anterior, se basa en el cierre positivo de otro defecto de CWG ( #232 ) que aún está activo. y por lo tanto, nada de eso se agrega a la norma.
Permítame citar una parte de un comentario de James McNellis a una respuesta a una pregunta similar de desbordamiento de pila:
No creo que el defecto 315 de CWG esté tan "cerrado" como su presencia en la página de "problemas cerrados" implica. El razonamiento dice que debería estar permitido porque "* p no es un error cuando p es nulo a menos que el valor de l se convierta en un valor de r". Sin embargo, eso se basa en el concepto de un "valor vacío", que forma parte de la resolución propuesta para el defecto 232 de CWG, pero que no se ha adoptado.
Recientemente probé el siguiente programa y lo compila, funciona bien y produce el resultado esperado en lugar de cualquier error de tiempo de ejecución.
#include <iostream>
class demo
{
public:
static void fun()
{
std::cout<<"fun() is called/n";
}
static int a;
};
int demo::a=9;
int main()
{
demo* d=nullptr;
d->fun();
std::cout<<d->a;
return 0;
}
Si se usa un puntero sin inicializar para acceder a la clase y / o el comportamiento de los miembros de la estructura no está definido, pero por eso se le permite acceder a los miembros estáticos utilizando punteros nulos también. ¿Hay algún daño en mi programa?
Del Proyecto de Norma C33 N3337:
9.4 miembros estáticos
2 Se puede hacer referencia a un miembro
static
de la claseX
usando la expresión de id-calificadoX::s
; no es necesario usar la sintaxis de acceso de miembro de clase (5.2.5) para referirse a un miembrostatic
. Se puede hacer referencia a un miembrostatic
utilizando la sintaxis de acceso de miembro de clase, en cuyo caso se evalúa la expresión del objeto.
Y en la sección sobre expresión de objetos ...
5.2.5 Acceso a los miembros de la clase
4 Si se declara que E2 tiene el tipo "referencia a T", entonces E1.E2 es un lvalor; el tipo de E1.E2 es T. De lo contrario, se aplicará una de las siguientes reglas.
- Si
E2
es un miembro de datosstatic
y el tipo deE2
esT
, entoncesE1.E2
es un lvalue; La expresión designa al miembro nombrado de la clase. El tipo deE1.E2
es T.
Basado en el último párrafo de la norma, las expresiones:
d->fun();
std::cout << d->a;
funciona porque ambos designan al miembro nombrado de la clase independientemente del valor de d
.
Lo que está viendo aquí es lo que consideraría una elección de diseño mal concebida y desafortunada en la especificación del lenguaje C ++ y muchos otros lenguajes que pertenecen a la misma familia general de lenguajes de programación.
Estos idiomas le permiten referirse a miembros estáticos de una clase usando una referencia a una instancia de la clase. Por supuesto, el valor real de la referencia de instancia se ignora, ya que no se requiere ninguna instancia para acceder a miembros estáticos.
Entonces, en d->fun();
El compilador utiliza el puntero d
solo durante la compilación para darse cuenta de que se está refiriendo a un miembro de la clase de demo
, y luego lo ignora. El compilador no emite ningún código para eliminar la referencia al puntero, por lo que no importa el hecho de que va a ser NULL durante el tiempo de ejecución.
Entonces, lo que ve que sucede está en perfecta conformidad con la especificación del lenguaje, y en mi opinión, la especificación sufre a este respecto, porque permite que ocurra algo ilógico: usar una referencia de instancia para referirse a un miembro estático.
PS La mayoría de los compiladores en la mayoría de los idiomas son capaces de emitir advertencias para ese tipo de cosas. No sé acerca de su compilador, pero es posible que desee verificarlo, ya que el hecho de no haber recibido ninguna advertencia por hacer lo que hizo puede significar que no tiene suficientes advertencias activadas.
TL; DR : Su ejemplo está bien definido. El simple hecho de desreferenciar un puntero nulo no invoca a UB.
Hay mucho debate sobre este tema, que básicamente se reduce a si la indirección a través de un puntero nulo es UB.
Lo único cuestionable que sucede en su ejemplo es la evaluación de la expresión del objeto. En particular, d->a
es equivalente a (*d).a
acuerdo con [expr.ref] / 2:
La expresión
E1->E2
se convierte a la forma equivalente(*(E1)).E2
; el resto de 5.2.5 abordará solo la primera opción (punto).
*d
se acaba de evaluar:
La expresión postfix antes de que se evalúe el punto o la flecha; El resultado de esa evaluación, junto con la expresión-id , determina el resultado de toda la expresión de postfix.
65) Si se evalúa la expresión de acceso de los miembros de la clase, la evaluación de la subexpresión se produce incluso si el resultado no es necesario para determinar el valor de toda la expresión de posfijo, por ejemplo, si la expresión-id denota un miembro estático.
Extraigamos la parte crítica del código. Considere la expresión de la expresión
*d;
En esta declaración, *d
es una expresión de valor descartada según [stmt.expr]. Entonces *d
solo se evalúa 1 , al igual que en d->a
.
Por lo tanto, si *d;
es válida, o en otras palabras, la evaluación de la expresión *d
, así es su ejemplo.
¿La indirección a través de punteros nulos resulta inherentemente en un comportamiento indefinido?
Existe la edición abierta del CWG #232 , creada hace más de quince años, que concierne a esta pregunta exacta. Se plantea un argumento muy importante. El informe comienza con
Al menos un par de lugares en el estado de IS que indirecciona a través de un puntero nulo produce un comportamiento indefinido: 1.9 [intro.execution] párrafo 4 da "dereferencing el puntero nulo" como un ejemplo de comportamiento indefinido, y 8.3.2 [dcl.ref ] el párrafo 4 (en una nota) utiliza este comportamiento supuestamente indefinido como justificación para la inexistencia de "referencias nulas".
Tenga en cuenta que el ejemplo mencionado se cambió para cubrir las modificaciones de los objetos const
lugar, y la nota en [dcl.ref], aunque todavía existe, no es normativa. El pasaje normativo fue eliminado para evitar el compromiso.
Sin embargo, 5.3.1 [expr.unary.op] el párrafo 1, que describe al operador "
*
" unario, no dice que el comportamiento no esté definido si el operando es un puntero nulo, como podría esperarse. Además, al menos un pasaje da a la desreferenciación un comportamiento bien definido de puntero nulo: 5.2.8 [expr.typeid] el párrafo 2 diceSi la expresión lvalue se obtiene aplicando el operador unary * a un puntero y el puntero es un valor de puntero nulo (4.10 [conv.ptr]), la expresión de typeid lanza la excepción bad_typeid (18.7.3 [bad.typeid]).
Esto es inconsistente y debe ser limpiado.
El último punto es especialmente importante. La cita en [expr.typeid] todavía existe y se relaciona con valores de tipo de clase polimórficos, que es el caso en el siguiente ejemplo:
int main() try {
// Polymorphic type
class A
{
virtual ~A(){}
};
typeid( *((A*)0) );
}
catch (std::bad_typeid)
{
std::cerr << "bad_exception/n";
}
El comportamiento de este programa está bien definido (se lanzará y capturará una excepción), y la expresión *((A*)0)
se evalúa, ya que no forma parte de un operando no evaluado. Ahora, si indirección a través de punteros nulos indujo UB, entonces la expresión escrita como
*((A*)0);
estaría haciendo precisamente eso, induciendo UB, lo que parece absurdo en comparación con el escenario de los typeid
. Si la expresión anterior se evalúa simplemente ya que cada expresión de valor descartado es 1 , ¿cuál es la diferencia crucial que hace la evaluación en el segundo fragmento de código UB? No hay una implementación existente que analice el typeid
operand, encuentra la referencia más interna y correspondiente y rodea a su operando con una comprobación: también se producirá una pérdida de rendimiento.
Una nota en ese tema termina la breve discusión con:
Estuvimos de acuerdo en que el enfoque en la norma parece estar bien:
p = 0; *p;
p = 0; *p;
no es inherentemente un error Una conversión de lvalue a rvalue le daría un comportamiento indefinido.
Es decir, el comité estuvo de acuerdo con esto. Aunque la resolución propuesta de este informe, que introdujo los llamados " valores vacíos ", nunca fue adoptada ...
Sin embargo, "no modificable" es un concepto de tiempo de compilación, mientras que en realidad se trata de valores de tiempo de ejecución y, por lo tanto, debería producir un comportamiento indefinido. Además, hay otros contextos en los que se pueden producir valores, como el operando izquierdo de. o. *, que también debe ser restringido. Se requiere redacción adicional.
... Eso no afecta a la justificación . Entonces, nuevamente, debe tenerse en cuenta que este problema incluso precede a C ++ 03, lo que lo hace menos convincente mientras nos acercamos a C ++ 17.
El número #315 parece cubrir su caso también:
Otra instancia a considerar es la de invocar una función miembro desde un puntero nulo:
struct A { void f () { } }; int main () { A* ap = 0; ap->f (); }
[…]
Justificación (octubre de 2003):
Acordamos que el ejemplo debe ser permitido.
p->f()
se reescribe como(*p).f()
acuerdo con 5.2.5 [expr.ref].*p
no es un error cuandop
es nulo a menos que el lvalue se convierta en un rvalue (4.1 [conv.lval]), que no está aquí.
De acuerdo con este razonamiento, la indirección a través de un puntero nulo per se no invoca a UB sin más conversiones de valores de valores a valores (= accesos a valores almacenados), enlaces de referencia, cálculos de valores o similares. (Nota bene: llamar a una función miembro no estática con un puntero nulo debería invocar a UB, aunque no esté permitido por [class.mfct.non-static] / 2. La razón está obsoleta a este respecto.)
Es decir, una mera evaluación de *d
no es suficiente para invocar UB. La identidad del objeto no es obligatoria, y tampoco lo es su valor almacenado previamente. Por otro lado, por ejemplo
*p = 123;
no está definido ya que hay un cálculo de valor del operando izquierdo, [expr.ass] / 1:
En todos los casos, la asignación se secuencia después del cálculo del valor de los operandos derecho e izquierdo
Debido a que se espera que el operando izquierdo sea un glvalue, la identidad del objeto al que hace referencia ese glvalue debe determinarse como se menciona en la definición de evaluación de una expresión en [intro.execution] / 12, lo cual es imposible (y por lo tanto lleva a a la UB).
1 [expr] / 11:
En algunos contextos, una expresión solo aparece por sus efectos secundarios. Dicha expresión se denomina expresión de valor descartado . La expresión se evalúa y su valor se descarta. […]. La conversión de lvalue a rvalue (4.1) se aplica si y solo si la expresión es un glvalue de tipo calificado volátil y […]