Определено ли поведение для ссылки на ранний элемент из более позднего выражения элемента во время инициализации агрегата?
Рассмотрим следующее:
struct mystruct
{
int i;
int j;
};
int main(int argc, char* argv[])
{
mystruct foo{45, foo.i};
std::cout << foo.i << ", " << foo.j << std::endl;
return 0;
}
Обратите внимание на использование foo.i
в списке агрегатов-инициализаторов.
g++ 5.2.0
выходы
45, 45
Является ли это четко определенным поведением? Является ли foo.i
в этом агрегаторе-инициализаторе всегда гарантировано ссылаться на созданный структурой элемент i
(и &foo.i
будет ссылаться на этот адрес памяти, например)?
Если я добавлю явный конструктор в mystruct
:
mystruct(int i, int j) : i(i), j(j) { }
Затем я получаю следующие предупреждения:
main.cpp:15:20: warning: 'foo.a::i' is used uninitialized in this function [-Wuninitialized]
a foo{45, foo.i};
^
main.cpp:19:34: warning: 'foo.a::i' is used uninitialized in this function [-Wuninitialized]
cout << foo.i << ", " << foo.j << endl;
Компиляция кода и выход:
45, 0
Ясно, что это делает что-то другое, и я предполагаю, что это поведение undefined. Это? Если да, то почему разница между этим и отсутствием конструктора? И как я могу получить начальное поведение (если это было четкое поведение) с определяемым пользователем конструктором?
Ответы
Ответ 1
В вашем втором случае поведение undefined, вы больше не используете агрегатную инициализацию, оно все еще содержит инициализацию списка, но в этом случае вы вызываете вызываемый пользователем конструктор. Чтобы передать второй аргумент вашему конструктору, ему нужно оценить foo.i
, но он еще не инициализирован, так как вы еще не вошли в конструктор и, следовательно, вы создаете неопределенное значение и создание неопределенного значения - undefined поведение.
У нас также есть раздел 12.7
Конструкция и разрушение [class.cdtor], в котором говорится:
Для объекта с нетривиальным конструктором, ссылающегося на любой нестатический член или базовый класс объекта перед тем как конструктор начнет выполнение результатов в undefined поведение [...]
Итак, я не вижу способа заставить ваш второй пример работать как ваш первый пример, предполагая, что первый пример действительно действителен.
Ваш первый случай кажется, что он должен быть хорошо определен, но я не могу найти ссылку в проекте стандарта, который, кажется, делает это явным. Возможно, это дефект, но в противном случае это будет undefined поведение, поскольку стандарт не определяет поведение. Стандарт говорит нам, что инициализаторы оцениваются по порядку, а побочные эффекты секвенированы, из раздела 8.5.4
[dcl.init.list]:
В списке инициализаторов списка с привязкой-инициализацией предложения инициализатора, включая все, что получается из пакета разложения (14.5.3), оцениваются в том порядке, в котором они появляются. То есть вычисление всех значений и побочный эффект, связанный с заданным предложением инициализатора, секвенирован перед каждым вычислением значения и стороной эффект, связанный с любым предложением инициализатора, которое следует за ним в разделенном запятыми списке списка инициализаторов. [...]
но у нас нет явного текста, говорящего о том, что члены инициализируются после каждого элемента.
MSalters утверждает, что раздел 1.9
, который гласит:
Доступ к объекту, обозначенному изменчивым значением glvalue (3.10), изменением объекта, вызовом библиотечного ввода-вывода функция или вызов функции, которая выполняет любую из этих операций , являются побочными эффектами, которые являются изменениями в состояние среды исполнения. [...]
в сочетании с:
[...] вычисление очень больших значений и побочный эффект, связанные с заданным предложением инициализатора, секвенируются перед каждым вычислением значения и побочным эффектом, связанным с любым последующим предложением инициализатора [...]
Достаточно, чтобы гарантировать, что каждый член агрегата инициализируется, когда оцениваются элементы списка инициализатора. Хотя это не будет применяться до С++ 11, поскольку порядок оценки списка инициализаторов не указан.
Для справки, если стандарт не налагает требования, поведение undefined из раздела 1.3.24
, которое определяет поведение undefined:
для которого настоящий международный стандарт не предъявляет никаких требований [Примечание: поведение undefined можно ожидать, если в этом Международном стандарте отсутствует явное определение поведение или [...]
Обновить
Johannes Schaub указывает отчет о дефекте 1343: последовательность инициализации неклассификации и темы обсуждения std Является ли копирование-инициализация агрегатного члена связанной с соответствующим предложением-инициализатором? и Является ли копирование инициализации агрегата член, связанный с соответствующим параметром-инициализатором?, которые являются релевантными.
Они в основном указывают, что первый случай в настоящее время не указан, я quote Richard Smith:
Итак, единственный вопрос - это побочный эффект инициализации s.i "связанный с" оценкой полного выражения "5"? я думаю единственное разумное предположение состоит в том, что оно: если 5 инициализировали член типа класса, вызов конструктора, очевидно, будет частью полное выражение по определению в [intro.execution] p10, поэтому оно естественно предположить, что то же самое верно для скалярных типов.
Однако, я не думаю, что стандарт действительно прямо говорит об этом в любом месте.
Итак, хотя, как указано в нескольких местах, похоже, что текущие реализации делают то, что мы ожидаем, кажется неразумным полагаться на него, пока это официально не будет разъяснено, или реализации не гарантируют.
Ответ 2
Из [dcl.init.aggr] 8.5.1 (2)
Когда агрегат инициализируется списком инициализатора, как указано в 8.5.4, элементы списка инициализаторов берутся как инициализаторы для членов агрегата, увеличивая индекс или порядок членов. Каждый член инициализируется копией из соответствующего предложения initializer.
акцент мой
И
В списке инициализаторов списка с привязкой к инициализации предложения инициализатора, включая все, что является результатом разложений пакетов (14.5.3), оцениваются в том порядке, в котором они отображаются. То есть вычисление каждого значения и побочный эффект, связанный с заданным предложением инициализатора, секвенируются перед каждым вычислением значения и побочным эффектом, связанным с любым предложением инициализатора, которое следует за ним в списке списка инициализаторов, разделенных запятыми.
Позволяет мне полагать, что каждый член класса будет инициализирован в том порядке, в котором они объявлены в списке инициализаторов, и поскольку foo.i
инициализируется до того, как мы его инициализируем для инициализации j
, это должно быть определено поведение.
Это также поддерживается [intro.execution] 1.9 (12)
Доступ к объекту, обозначенному изменчивым значением glvalue (3.10), изменением объекта, вызовом функции ввода-вывода библиотеки или вызовом функции, которая выполняет любую из этих операций эффекты, которые являются изменениями состояния среды выполнения.
акцент мой
В вашем втором примере мы не используем инициализацию агрегата, но инициализируем список. [dcl.init.list] 8.5.4 (3) имеет
Список-инициализация объекта или ссылки типа T определяется следующим образом:
[...]
- В противном случае, если T - тип класса, рассматриваются конструкторы. Соответствующие конструкторы перечислены и лучший выбирается с помощью разрешения перегрузки (13.3, 13.3.1.7).
Итак, теперь мы будем называть ваш конструктор. При вызове конструктора foo.i
не был инициализирован, поэтому мы копируем неинициализированную переменную, которая является undefined.
Ответ 3
Моя первая идея была UB, но вы полностью в общем случае инициализации. Проект n4296 для спецификации С++ 11 явно указан в пункте 8.5.1 Aggregates [dcl.init.aggr]:
Агрегат - это массив или класс без конструкторов, предоставляемых пользователем, без частных или защищенных нестатических элементов данных, без базовых классов и без виртуальных функций
Далее:
Когда агрегат инициализируется списком инициализаторов, как указано в 8.5.4, элементы списка инициализаторов берутся как инициализаторы для членов совокупности, в возрастающем индексе или порядке членов
(подчеркните мое)
Я понимаю, что mystruct foo{45, foo.i};
сначала инициализирует foo.i
с 45, затем foo.j
с foo.i
.
Я бы не рискнул использовать это в реальном коде, так как даже если я считаю, что он определен стандартом, я бы боялся, что программист-программист подумал иначе...
Ответ 4
как я могу получить начальное поведение (если это было корректное поведение) с определяемым пользователем конструктором?
Передача параметра по ссылке для этого параметра, которая ссылается на ранее инициализированный параметр объекта-объекта, следующим образом:
mystruct(int i, int& j):i(i),j(j)