Подтвердить что ты не робот

С++ 11 странное поведение инициализации скобки

Я не понимаю, как работают правила инициализации С++ 11. Имея этот код:

struct Position_pod {
    int x,y,z;
};

class Position {
public:
    Position(int x=0, int y=0, int z=0):x(x),y(y),z(z){}
    int x,y,z;
};

struct text_descriptor {
    int             id;
    Position_pod    pos;
    const int       &constNum;
};

struct text_descriptor td[3] = {
     {0, {465,223}, 123},
     {1, {465,262}, 123},
};

int main() 
{
    return 0;
}

Обратите внимание, что массив объявлен как имеющий 3 элемента, но предоставляется только 2 инициализатора.

Однако он компилируется без ошибок, что звучит странно, поскольку последний элемент ссылки элемента массива будет неинициализирован. Действительно, оно имеет значение NULL:

(gdb) p td[2].constNum 
$2 = (const int &) @0x0: <error reading variable>

И теперь "волшебство": я изменил Position_pod на Position

struct text_descriptor {
    int             id;
    Position_pod    pos;
    const int       &constNum;
};

становится следующим:

struct text_descriptor {
    int             id;
    Position        pos;
    const int       &constNum;
};

и теперь он дает ожидаемую ошибку:

error: uninitialized const member ‘text_descriptor::constNum'

Мой вопрос: почему он компилируется в первом случае, когда он должен давать ошибку (как во втором случае). Разница в том, что Position_pod использует инициализацию скобки стиля C, а Position использует инициализацию стиля С++ 11, которая вызывает конструктор Position. Но как это может повлиять на возможность оставить ссылочный член неинициализированным?

(Обновление) Составитель: gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

4b9b3361

Ответ 1

Ясно, что

struct text_descriptor td[3] = {
     {0, {465,223}, 123},
     {1, {465,262}, 123},
};

- это инициализация списка, а список инициализации не пуст.

С++ 11 говорит ([dcl.init.list] p3):

Список-инициализация объекта или ссылки типа T определяется следующим образом:

  • Если в списке инициализаторов нет элементов, а T - это тип класса с конструктором по умолчанию, объект инициализируется значением.
  • В противном случае, если T является агрегатом, выполняется агрегатная инициализация (8.5.1).
  • ...

[dcl.init.aggr] р1:

Агрегат - это массив или класс (раздел 9) без конструкторов, предоставляемых пользователем (12.1), без инициализаторов скобок или равных для нестатических элементов данных (9.2), без частных или защищенных нестатических (раздел 11), нет базовых классов (раздел 10) и нет виртуальных функций (10.3).

td - это массив, поэтому он является агрегатом, поэтому выполняется агрегатная инициализация.

[dcl.init.aggr] P7:

Если в списке меньше предложений-инициализаторов, чем в агрегате, то каждый член, явно не инициализированный, должен быть инициализирован из пустого списка инициализаторов (8.5.4).

Это здесь, поэтому td[2] инициализируется из пустого списка инициализации, который снова ([dcl.init.list] p3) означает, что он инициализирован значением.

Значение-инициализация, в свою очередь, означает ([dcl.init] p7):

Для инициализации значения объекта типа T означает:

  • если T является (возможно, cv-квалифицированным) типом класса (раздел 9) с предоставленным пользователем конструктором (12.1),...
  • Если T является (возможно, cv-квалифицированным) классом неединичного класса без конструктора, предоставленного пользователем, тогда объект инициализируется нулем и, если T неявно объявленный конструктор по умолчанию является нетривиальным, этот конструктор называется.
  • ...

Ваш класс text_descriptor - это класс без конструктора, предоставляемого пользователем, поэтому td[2] сначала инициализируется нулем, а затем вызывается его конструктор.

Средство нулевой инициализации ([dcl.init] p5):

Для нулевой инициализации объекта или ссылки типа T означает:

  • если T - скалярный тип (3.9),...
  • если T является (возможно, cv-квалифицированным) классом неединичного класса, каждый нестатический элемент данных и каждый подобъект базового класса инициализируются нулем, а заполнение инициализируется нулевыми битами;
  • если T является (возможно, cv-квалифицированным) типом объединения,...
  • if T - тип массива,...
  • Если T является ссылочным типом, инициализация не выполняется.

Это корректно определено независимо от конструктора по умолчанию text_descriptor: он просто равен нулю - инициализирует не ссылочные члены и под-члены.

Затем вызывается конструктор по умолчанию, если он нетривиален. Здесь, как определяется конструктор по умолчанию ([special] p5):

Конструктор по умолчанию для класса X является конструктором класса X, который может быть вызван без аргумента. Если для класса X нет объявленного пользователем конструктора, конструктор без параметров неявно объявляется по умолчанию (8.4). Неявно объявленный конструктор по умолчанию является встроенным публичным членом своего класса. По умолчанию конструктор по умолчанию для класса X определяется как удаленный, если:

  • ...
  • любой нестатический элемент данных без элемента с выравниванием или равным значением имеет ссылочный тип,
  • ...

Конструктор по умолчанию является тривиальным, если он не предоставляется пользователем, и если:

  • его класс не имеет виртуальных функций (10.3) и нет виртуальных базовых классов (10.1) и
  • нет нестатического элемента данных его класса, который имеет инициализатор скобок или равный, и
  • все прямые базовые классы его класса имеют тривиальные конструкторы по умолчанию, а
  • для всех нестатических членов данных своего класса, которые относятся к типу класса (или его массиву), каждый такой класс имеет тривиальный конструктор по умолчанию.

В противном случае конструктор по умолчанию является нетривиальным.

Таким образом, неявно определенный конструктор удаляется, как и ожидалось, но он также является тривиальным, если pos - тип POD (!). Поскольку конструктор тривиален, он не вызывается. Поскольку конструктор не вызывается, тот факт, что он удален, не является проблемой.

Это отверстие в С++ 11, которое с тех пор исправлено. Скорее всего, это было связано с недоступными тривиальными конструкторами по умолчанию, но исправленная формулировка также охватывает удаленные тривиальные конструкторы по умолчанию. N4140 (примерно С++ 14) говорит в [dcl.init.aggr] p7 (акцент мой):

  • Если T является (возможно, cv-qualit) типом класса без предоставленного пользователем или удаляемого конструктора по умолчанию, тогда объект инициализируется нулем и проверяются семантические ограничения для инициализации по умолчанию, и если T имеет нетривиальный конструктор по умолчанию, объект инициализируется по умолчанию;

Как T.C. отметил в комментариях, другой DR также изменился, так что td[2] по-прежнему инициализируется из пустого списка инициализации, но этот пустой список инициализации теперь подразумевает агрегатную инициализацию. Это, в свою очередь, подразумевает, что каждый из членов td[2] также инициализируется из пустого списка инициализации ([dcl.init.aggr] p7), поэтому, похоже, инициализирует ссылочный элемент из {}.

[dcl.init.aggr] p9 затем говорит (как пояснил Remyabel в теперь удаленном ответе):

Если неполный или пустой список инициализаторов оставляет член ссылочного типа неинициализированным, программа плохо сформирована.

Мне непонятно, что это относится к ссылкам, инициализированным из неявного {}, но компиляторы действительно интерпретируют его как таковое, и нет ничего другого, что могло бы означать это.

Ответ 2

Первая версия (одна с суффиксом _pod) все еще работает, но не дает ошибки, потому что для значения z выбрано значение по умолчанию для int (0). Идем для ссылки const int.

Во второй версии вы не можете определить ссылку на const, не давая ей значения. Компилятор дает вам сообщение об ошибке, потому что позже вы не можете присвоить ему какое-либо значение.

Кроме того, используемый вами компилятор играет важную роль, возможно, это ошибка, просто потому, что вы объявляете член класса перед членом int.