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

Me sorprende descubrir accidentalmente que los siguientes trabajos:

#include  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 en su propio constructor. Esto parece una definición circular en el nivel de fuente. ¿Los estándares realmente le permiten pasar un objeto a una función antes de que el objeto se construya o es este comportamiento indefinido?

Supongo que no es tan extraño dado que todas las funciones miembro 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 comstackción.

Nota, NO estoy preguntando si esto es útil o una buena idea; Estoy retocando para aprender más sobre las clases.

Esto no es un comportamiento indefinido. Aunque foo no está inicializado, lo está usando de una manera permitida por el estándar. Después de asignar espacio para un objeto, pero antes de que esté completamente inicializado, puede usarlo de manera limitada. Ambas vinculando una referencia a esa variable y tomando su dirección están permitidas.

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

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

  #include  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 comstackr e imprimir:

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

y la resolución fue:

3.8 [basic.life] El párrafo 6 indica que las referencias aquí son válidas. Está permitido tomar la dirección de un objeto de clase antes de que esté completamente inicializado, y se le permite pasarlo como un argumento a un parámetro de referencia, siempre que la referencia se pueda vincular directamente. Excepto por el hecho de no convertir los punteros en void * para el% p en los printfs, estos ejemplos son conformes 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:

De manera similar, antes de que se haya iniciado el tiempo de vida de un objeto pero después de que se haya asignado el almacenamiento que el objeto ocupará o, después de que la vida útil de un objeto haya finalizado y antes de que el almacenamiento ocupado sea reutilizado o liberado, cualquier valor de gl que se refiera a el objeto original puede ser usado pero solo de manera limitada. Para un objeto en construcción o destrucción, ver 12.7. De lo contrario, un glvalue se refiere al almacenamiento asignado (3.7.4.2), y el uso de las propiedades del glvalue que no dependen de su valor está bien definido. El progtwig tiene un comportamiento indefinido si:

  • se aplica una conversión lvalue-r-valor (4.1) a dicho valor gl,

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

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

  • el glvalue se usa como el operando de un dynamic_cast (5.2.7) o como el operando de typeid.

No estamos haciendo nada con foo que caiga bajo un comportamiento indefinido como se define en las viñetas anteriores.

Si intentamos esto con Clang, vemos una advertencia siniestra ( véalo 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álida. Por otro lado, el siguiente ejemplo de autoinicialización del borrador del estándar C ++ 11 :

 int x = x ; 

no invoca comportamiento indefinido.

Problema activo 453: las referencias solo pueden vincularse a objetos “válidos” que también parecen relevantes pero que aún están abiertos. El lenguaje propuesto inicial es consistente con el Informe de defectos 363.

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

Dado que se trata de memoria asignada y alineada, podemos referirnos a ella utilizando expresiones lvalue del tipo Foo (es decir, Foo& ). Lo que aún no podemos hacer es tener una conversión de lvalue a rvalue. Eso solo está permitido después de que se ingrese el cuerpo 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 ingresado el cuerpo constructor, existe un objeto Foo y sus miembros pueden leerse.

Esto nos deja con un pequeño detalle, y esa es la búsqueda de nombre. En Foo foo(foo) , el primer foo introduce el nombre en el scope 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.