Каково ожидаемое поведение ссылки на ранее установленные поля при объявлении начальных значений структуры с использованием составного литерала: В частности, можно ли: struct foo v = { .v1 = ..., .v2 = .v1+1 };
Более полный пример:
struct s {
int i1 ;
int i2 ;
} ;
void func(void)
{
// Case A:
struct s v1 = { 3 } ;
v1 = (struct s) ( .i1 = 4, .i2 = v1.i1+10 }
// always v1.i2=13, NOT 14
// Case B:
// Is this legal ?
struct s v2 = ( .i1 = 1, .i2 = v2.i1+10 }
// Under GCC, v2.i2 = 11
}
В случае A - стандарт ясно, что создается «временный» составной литерал на основе значения v1 перед присвоением (v1.i1=3), в результате чего v2.i2 = 13. Объяснение состоит в том, что это в основном:
struct s v1 = { 3 } ;
struct s temp = ( .i1 = 4, .i2 = v1.i1+10 }
v1 = temp ;
Для случая B, по крайней мере, с GCC, выглядит так, как будто можно ссылаться на ранее установленные значения в том же операторе SAME - в этом случае i2 установлено в 11, подразумевая, что это НЕ было реализовано с использованием временной переменной, которая имела бы результаты в v2. я2 = 10;
// NOT the same as struct v1 = { ... };
struct s temp = ( .i1 = 4, .i2 = v1.i1+10 }
struct s v1 = temp ;
Мой вопрос: что говорит стандарт C99/C23 о втором случае? это обязательное поведение, поведение, специфичное для реализации, или неопределенное поведение, которое компиляторы должны помечать как предупреждение/ошибку?
Я попробовал описанное выше с GCC9 (-Wall, -Wextra), а также с Coverity - никаких проблем не возникло.
Случай A сложен, поскольку {3}
является инициализацией v1.i1
, а v1.i2
инициализируется 0
по умолчанию. Теперь v1 = (struct s) { .i1 = 4, .i2 = v1.i1+10 }
— это задание (обратите внимание на фиксированный {
). На этом этапе v1.i1 == 3
из инициализации, поскольку нет идентификатора v1
, связанного с составным литералом. Таким образом, результат будет v1.v2 == 13
, а не 14
.
О, добавьте -pedantic -Werror
в строку компиляции, и появится ваш -Wmissing-field-initializers
(жалоба на оригинал {3}
).
Стандарт C ничего не говорит о том, какие элементы или члены агрегатов инициализируются до оценки каких-либо инициализаторов.
Стандарт содержит формулировку о порядке инициализации элементов или членов (в проекте C 2023 N3096 6.7.10). Однако в лучшем случае этот порядок сообщает нам только порядок, в котором значения хранятся в элементах или членах. Здесь ничего не говорится о том, когда эти значения были подготовлены. 6.7.10 24 говорит:
Вычисления выражений списка инициализации имеют неопределенную последовательность друг относительно друга, и поэтому порядок возникновения побочных эффектов не указан.
Эта формулировка впервые появилась в C 2011 6.7.9 23 (определено из проекта N1570). (Тот факт, что он отсутствовал до этого, не означает, что существовало секвенирование; это означает, что стандарт C ничего не говорил об этом и, следовательно, ничего не утверждал о секвенировании.) Эта формулировка подтверждает, что выражения не обязательно вычисляются в том же порядке, что и элементы или члены инициализируются, поскольку это наложит на них порядок.
Таким образом, в struct s v2 = ( .i1 = 1, .i2 = v2.i1+10 }
компилятор может сгенерировать программу, которая работает в следующем порядке:
1
.v2.i1+10
..i1
для результата шага 1..i2
для результата шага 2.В этом порядке v2.i1
не инициализируется во время выполнения шага 2.
Другие допустимые порядки включают 2 1 3 4 и 1 3 2 4, а также переупорядочение, разделяющее v2.i1+10
на оценки v2.i1
, 10
и сложение.
Основная цель определения порядка инициализации состоит в том, чтобы определить, какие элементы или члены инициализируются, когда обозначения отсутствуют. То есть при разборе кода компилятор принимает инициализаторы, соответствующие элементам или членам в том порядке, в котором они появляются в агрегате. Вторичная цель фигурирует в проекте C 2020 N3096 6.7.10 20:
Инициализация должна происходить в порядке списка инициализаторов, каждый инициализатор, предоставленный для конкретного подобъекта, переопределяет любой ранее указанный инициализатор для того же подобъекта;…
Это говорит нам, какой инициализатор используется, если элемент или член появляется при инициализации несколько раз, но опять же ничего не говорится о порядке, в котором оцениваются инициализаторы.
Я подозреваю, что обсуждение «порядка» в C 2023 6.7.10 было мотивировано исключительно для того, чтобы указать интерпретацию того, какие инициализаторы применяются к каким элементам или членам, и не предназначалось для того, чтобы оказать какое-либо влияние на состояние программы в работающей программе. Единственный способ, которым я вижу, что это может повлиять на наблюдаемое поведение программы (кроме попыток ссылаться на ранее инициализированные элементы или члены), - это наличие инициализируемых изменчивых объектов. Это вызовет некоторые проблемы. Является ли инициализация доступом к объекту с целью volatile
? Если элемент или член появляется дважды при инициализации, означает ли тот факт, что второе появление «переопределяет» первое, первое подавляется и никогда не появляется, или что оно появляется, но позже перезаписывается?
Если я правильно прочитал эту ссылку на инициализацию структуры, случай B действителен. Переменная существует, когда происходит инициализация (именно поэтому, например,
int a = a;
будет строиться), а инициализация членов структуры происходит в том порядке, в котором вы их записываете. Поэтому, когда вы инициализируетеi2
,v2.i1
уже будет инициализирован.