Передает ли объект С++ в свой собственный конструктор законным?

Я с удивлением обнаружил, что следующие работы:

#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...
}

Я передаю адрес сконструированного объекта в свой собственный конструктор. Это похоже на круговое определение на уровне источника. Действительно ли стандарты позволяют вам передать объект в функцию до того, как объект будет построен даже, или это поведение undefined?

Я полагаю, что это не так странно, учитывая, что все функции-члены класса уже имеют указатель на данные для своего экземпляра класса как неявный параметр. А макет элементов данных фиксируется во время компиляции.

Заметьте, я не спрашиваю, полезно ли это или хорошая идея; Я просто возился, чтобы узнать больше о занятиях.

Ответ 1

Это не поведение undefined, хотя foo неинициализирован, вы используете его таким образом, который разрешен стандартом. После того как пространство выделено для объекта, но до его полной инициализации вам разрешено использовать его в ограниченных путях. Разрешено связывать ссылку на эту переменную и принимать ее адрес.

Это распространяется на отчет о дефекте 363: Инициализация класса из себя, в котором говорится:

И если да, то какова семантика самоинициализации UDT? Например

 #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;
 }

можно скомпилировать и распечатать:

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

и разрешение было:

3.8 [basic.life], пункт 6, указывает, что ссылки здесь действительны. Он разрешил принимать адрес объекта класса до его полной инициализации и разрешал передавать его в качестве аргумента в ссылочный параметр, если ссылка может напрямую связываться. За исключением того, что указатели на void * не отображаются для% p в printfs, эти примеры соответствуют стандарту.

Полная цитата из раздела 3.8 [basic.life] из проекта С++ 14 выглядит следующим образом:

Аналогично, до начала жизни объекта, но после хранилище, которое будет занимать объект, или, после срок службы объекта закончился и перед хранилищем, которое объект, занятый, повторно используется или освобождается, любое значение gl, которое ссылается на оригинальный объект может использоваться, но только ограниченным образом. Для объекта под строительство или уничтожение, см. 12.7. В противном случае такое значение glvalue относится к выделенному хранилищу (3.7.4.2) и использует свойства glvalue, которые не зависят от его значения, четко определены. Программа имеет поведение undefined, если:

  • для такого glvalue применяется преобразование lvalue-to-rval (4.1),

  • glvalue используется для доступа к нестатическому элементу данных или вызова нестатической функции-члена из объект или

  • glvalue привязан к ссылке на виртуальный базовый класс (8.5.3) или

  • glvalue используется как операнд dynamic_cast (5.2.7) или как операнд typeid.

Мы ничего не делаем с foo, который подпадает под поведение undefined, как определено выше указанными марками.

Если мы попробуем это с помощью clang, мы увидим зловещее предупреждение (посмотреть его в прямом эфире):

предупреждение: переменная 'foo' не инициализируется при использовании в своей собственной инициализации [-Wuninitialized]

Это действительное предупреждение, поскольку и неопределенное значение из неинициализированной автоматической переменной - это undefined поведение, но в этом случае вы просто привязываете ссылку и принимаете адрес переменной внутри конструктора, который не дает неопределенного значения и действителен. С другой стороны следующий пример самоинициализации из проекта С++ 11:

int x = x ;

вызывает поведение undefined.

Активная проблема 453: Ссылки могут привязываться только к "действительным" объектам, также кажется актуальным, но все еще открытым. Исходный предложенный язык соответствует отчету о дефектах 363.

Ответ 2

Конструктор вызывается в точке, где память выделяется для будущего объекта. В этот момент в этом месте нет объекта (или, возможно, объекта с тривиальным деструктором). Кроме того, указатель this ссылается на эту память, и память правильно выровнена.

Поскольку он выделяет и выравнивает память, мы можем ссылаться на него, используя выражения lvalue типа Foo (т.е. Foo&). То, что мы еще не можем сделать, это преобразование lvalue-to-rvalue. Это разрешено только после ввода тела конструктора.

В этом случае код просто пытается напечатать &bar внутри тела конструктора. Было бы даже законно печатать bar.member здесь. Поскольку тело конструктора введено, существует объект Foo, и его члены могут быть прочитаны.

Это оставляет нам одну небольшую деталь и этот поиск имени. В Foo foo(foo) первый Foo вводит имя в области видимости, а второй Foo поэтому ссылается на только что объявленное имя. Поэтому int x = x недействителен, но int x = sizeof(x) действителен.