c++ class constructor language-lawyer undefined-behavior

¿Es legal pasar un objeto C++ a su propio constructor?



class language-lawyer (2)

Me sorprende descubrir accidentalmente que lo siguiente funciona:

#include <iostream> int main(int argc, char** argv) { struct Foo { Foo(Foo& bar) { std::cout << &bar << std::endl; } }; Foo foo(foo); // I can''t believe this works... std::cout << &foo << std::endl; // but it does... }

Estoy pasando la dirección del objeto construido a su propio constructor. Esto parece una definición circular en el nivel de origen. ¿Los estándares realmente le permiten pasar un objeto a una función antes de que el objeto se construya o es un comportamiento indefinido?

Supongo que no es tan extraño dado que todas las funciones de miembros de la clase ya tienen un puntero a los datos para su instancia de clase como un parámetro implícito. Y el diseño de los miembros de datos se fija en tiempo de compilación.

Tenga en cuenta que NO pregunto si esto es útil o una buena idea; Solo estoy jugando para aprender más sobre las clases.


Este no es un comportamiento indefinido. Aunque foo no está inicializado, lo está utilizando de la manera permitida por el estándar. Después de que se asigna espacio para un objeto pero antes de que se inicialice completamente, se le permite usarlo de formas limitadas. Se permite vincular una referencia a esa variable y tomar su dirección.

Esto está cubierto por el informe de defectos 363: Inicialización de la clase desde uno mismo que dice:

Y si es así, ¿cuál es la semántica de la autoinicialización de UDT? Por ejemplo

#include <stdio.h> struct A { A() { printf("A::A() %p/n", this); } A(const A& a) { printf("A::A(const A&) %p %p/n", this, &a); } ~A() { printf("A::~A() %p/n", this); } }; int main() { A a=a; }

se puede compilar e imprimir:

A::A(const A&) 0253FDD8 0253FDD8 A::~A() 0253FDD8

y la resolución fue:

3.8 [basic.life] párrafo 6 indica que las referencias aquí son válidas. Se le permite tomar la dirección de un objeto de clase antes de que se inicialice por completo, y se le permite pasarlo como argumento a un parámetro de referencia siempre que la referencia pueda vincularse directamente. Excepto por el hecho de que los punteros no se pueden anular * para el% p en printfs, estos ejemplos cumplen con los estándares.

La cita completa de la sección 3.8 [basic.life] del borrador del estándar C ++ 14 es la siguiente:

Del mismo modo, antes de que comience la vida útil de un objeto, pero después de que se haya asignado el almacenamiento que ocupará el objeto o, después de que haya finalizado la vida útil de un objeto y antes del almacenamiento que el objeto ocupado se reutilice o se libere, cualquier valor de gl el objeto original puede usarse pero solo de manera limitada. Para un objeto en construcción o destrucción, ver 12.7. De lo contrario, dicho valor de gl se refiere al almacenamiento asignado (3.7.4.2), y el uso de las propiedades del valor de gl que no dependen de su valor está bien definido. El programa tiene un comportamiento indefinido si:

  • se aplica una conversión lvalue-to-rvalue (4.1) a dicho glvalue,

  • glvalue se usa para acceder a un miembro de datos no estático o llamar a una función de miembro no estático del objeto, o

  • el valor de gl está vinculado a una referencia a una clase base virtual (8.5.3), o

  • El valor de gl se utiliza como el operando de un dynamic_cast (5.2.7) o como el operando de typeid.

No estamos haciendo nada con foo que se encuentre bajo un comportamiento indefinido como se define en los puntos anteriores.

Si intentamos esto con Clang, vemos una advertencia ominosa ( verla en vivo ):

advertencia: la variable ''foo'' no se inicializa cuando se usa dentro de su propia inicialización [-Wuninitialized]

Es una advertencia válida ya que producir un valor indeterminado a partir de una variable automática no inicializada es un comportamiento indefinido . Sin embargo, en este caso, solo está vinculando una referencia y tomando la dirección de la variable dentro del constructor, que no produce un valor indeterminado y es válido. Por otro lado, el siguiente ejemplo de autoinicialización del borrador del estándar C ++ 11 :

int x = x ;

invoca un comportamiento indefinido.

Problema activo 453: las referencias solo se pueden unir a objetos "válidos" también parece relevante pero aún está abierto. El lenguaje propuesto inicial es consistente con el Informe de defectos 363.


Se llama al constructor en un punto donde se asigna memoria para el futuro objeto. En ese punto, no existe ningún objeto en esa ubicación (o posiblemente un objeto con un destructor trivial). Además, el puntero this refiere a esa memoria y la memoria está correctamente alineada.

Como está asignada y alineada, podemos referirnos a ella usando expresiones de valor de tipo Foo (es decir, Foo& ). Lo que aún no podemos hacer es tener una conversión de valor a valor. Eso solo se permite después de ingresar el cuerpo del constructor.

En este caso, el código solo intenta imprimir &bar dentro del cuerpo del constructor. Incluso sería legal imprimir bar.member aquí. Como se ha introducido el cuerpo del constructor, existe un objeto Foo y se pueden leer sus miembros.

Esto nos deja con un pequeño detalle, y esa es la búsqueda de nombres. En Foo foo(foo) , el primer foo introduce el nombre en su alcance y, por lo tanto, el segundo foo hace referencia al nombre recién declarado. Es por eso que int x = x no es válido, pero int x = sizeof(x) es válido.