Порядок оценки индексов массива (по сравнению с выражением) в C

Глядя на этот код:

static int global_var = 0;

int update_three(int val)
{
    global_var = val;
    return 3;
}

int main()
{
    int arr[5];
    arr[global_var] = update_three(2);
}

Какая запись массива обновляется? 0 или 2?

Есть ли в спецификации C часть, указывающая приоритет операции в данном конкретном случае?


person Jiminion    schedule 13.01.2020    source источник
comment
Это пахнет неопределенным поведением. Это определенно то, что никогда не следует преднамеренно кодировать.   -  person Fiddling Bits    schedule 13.01.2020
comment
Я согласен, что это пример плохого кодирования.   -  person Jiminion    schedule 13.01.2020
comment
Некоторые анекдотические результаты: godbolt.org/z/hM2Jo2   -  person Bob__    schedule 13.01.2020
comment
@Bob__ Я не уверен, но мне нужен был «статический int», чтобы мой код действительно работал.   -  person Jiminion    schedule 13.01.2020
comment
Это может быть признаком других проблем в вашем реальном коде. Добавление static не меняет текущего.   -  person Bob__    schedule 13.01.2020
comment
Это не имеет ничего общего с индексами массива или порядком операций. Это связано с тем, что спецификация C называет точками последовательности, и, в частности, с тем фактом, что выражения присваивания НЕ создают точку последовательности между левым и правым выражением, поэтому компилятор может делать все, что захочет.   -  person Lee Daniel Crocker    schedule 14.01.2020
comment
Вы должны сообщить о запросе функции clang, чтобы этот фрагмент кода вызывал предупреждение IMHO.   -  person malat    schedule 14.01.2020
comment
@Bob__ да, ты прав. IIRC, переменная, определенная на верхнем уровне (в том же контексте, что и main()), неявно становится статической.   -  person Jiminion    schedule 14.01.2020
comment
@malat: ИМХО, это необоснованное ожидание. Нельзя ожидать, что компилятор будет анализировать update_three на предмет присваивания global_var на тот случай, если это может привести к неопределенному поведению. Программист должен взять на себя некоторую ответственность!   -  person TonyK    schedule 15.01.2020
comment
@TonyK компилятор уже собирается проделать более глубокую работу по рассмотрению вопроса о том, следует ли и как встроить эту функцию.   -  person Daniel McLaury    schedule 15.01.2020


Ответы (5)


Порядок левых и правых операндов

Чтобы выполнить присваивание в arr[global_var] = update_three(2), реализация C должна оценить операнды и, как побочный эффект, обновить сохраненное значение левого операнда. C 2018 6.5.16 (о назначениях) параграф 3 говорит нам, что в левом и правом операндах нет последовательности:

Оценки операндов не упорядочены.

Это означает, что реализация C может сначала вычислить lvalue arr[global_var] (под «вычислением lvalue» мы подразумеваем определение того, к чему относится это выражение), затем вычислить update_three(2) и, наконец, присвоить значение последнего к первому; или сначала оценить update_three(2), затем вычислить lvalue, а затем присвоить первое второму; или оценить lvalue и update_three(2) каким-то смешанным образом, а затем присвоить правое значение левому lvalue.

Во всех случаях присвоение значения lvalue должно быть последним, потому что 6.5.16 3 также говорит:

… Побочный эффект обновления сохраненного значения левого операнда упорядочивается после вычисления значений левого и правого операндов…

Нарушение последовательности

Некоторые могут задуматься о неопределенном поведении из-за использования global_var и отдельного его обновления в нарушение 6.5 2, в котором говорится:

Если побочный эффект на скалярном объекте не является последовательным относительно другого побочного эффекта на тот же скалярный объект или вычисления значения с использованием значения того же скалярного объекта, поведение не определено…

Многим практикам C хорошо известно, что поведение таких выражений, как x + x++, не определено стандартом C, потому что они оба используют значение x и отдельно изменяют его в одном и том же выражении без последовательности. Однако в данном случае у нас есть вызов функции, обеспечивающий некоторую последовательность. global_var используется в arr[global_var] и обновляется при вызове функции update_three(2).

6.5.2.2 10 говорит нам, что перед вызовом функции есть точка следования:

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

Внутри функции global_var = val; является полным выражением, как и 3 в return 3; согласно 6.8 4:

Полное выражение — это выражение, которое не является частью другого выражения, а также частью декларатора или абстрактного декларатора…

Затем между этими двумя выражениями есть точка следования, опять же согласно 6.8 4:

… Существует точка последовательности между оценкой полного выражения и оценкой следующего полного выражения, которое нужно оценить.

Таким образом, реализация C может сначала оценить arr[global_var], а затем выполнить вызов функции, и в этом случае между ними будет точка последовательности, потому что она есть перед вызовом функции, или она может оценить global_var = val; в вызове функции, а затем arr[global_var], в котором случае между ними есть точка следования, потому что она стоит после полного выражения. Таким образом, поведение не определено — любая из этих двух вещей может быть оценена первой, — но оно не является неопределенным.

person Eric Postpischil    schedule 13.01.2020

Результат здесь не указан.

Хотя порядок операций в выражении, который определяет, как группируются подвыражения, четко определен, порядок вычисления не указан. В данном случае это означает, что либо global_var может быть прочитано первым, либо вызов update_three может произойти первым, но неизвестно, что именно.

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

Чтобы уточнить, стандарт C определяет неопределенное поведение в разделе 3.4.3 как:

неопределенное поведение

поведение при использовании непереносимой или ошибочной программной конструкции или ошибочных данных, для которых настоящий стандарт не предъявляет требований

и определяет неопределенное поведение в разделе 3.4.4 следующим образом:

неопределенное поведение

использование неопределенного значения или другое поведение, когда настоящий международный стандарт предоставляет две или более возможности и не налагает дополнительных требований, в отношении которых выбирается в любом случае

В стандарте указано, что порядок вычисления аргументов функции не указан, что в данном случае означает, что либо arr[0] устанавливается равным 3, либо arr[2] устанавливается равным 3.

person dbush    schedule 13.01.2020
comment
«вызов функции вводит точку последовательности» недостаточно. Если левый операнд вычисляется первым, этого достаточно, поскольку тогда точка последовательности отделяет левый операнд от вычислений в функции. Но если левый операнд оценивается после вызова функции, точка последовательности из-за вызова функции не находится между оценками в функции и оценкой левого операнда. Вам также потребуется точка последовательности, которая разделяет полные выражения. - person Eric Postpischil; 13.01.2020
comment
@EricPostpischil В терминологии до C11 есть точка последовательности при входе и выходе из функции. В терминологии C11 все тело функции имеет неопределенную последовательность относительно вызывающего контекста. Они оба указывают одно и то же, просто используя разные термины. - person M.M; 13.01.2020
comment
Это абсолютно неправильно. Порядок оценки аргументов присваивания не указан. Что касается результата этого конкретного назначения, то это создание массива с ненадежным содержимым, как непереносимым, так и изначально неверным (несовместимым с семантикой или любым из предполагаемых результатов). Идеальный случай неопределенного поведения. - person kuroi neko; 15.01.2020
comment
@kuroineko Тот факт, что вывод может варьироваться, не делает его поведение неопределенным автоматически. В стандарте есть разные определения неопределенного и неопределенного поведения, и в данной ситуации это последнее. - person dbush; 15.01.2020
comment
@EricPostpischil Здесь у вас есть точки последовательности (из информационного приложения C C11): между оценками указателя функции и фактическими аргументами в вызове функции и фактическим вызовом. (6.5.2.2), Между вычислением полного выражения и следующим вычисляемым полным выражением... /--/ ...выражение (необязательное) в операторе возврата (6.8.6.4). Ну и в каждой точке с запятой тоже, так как это полное выражение. - person Lundin; 15.01.2020
comment
@kuroineko Зависимость от неопределенного поведения делает именно это: создает мусорные значения и т. Д. Но она не делает совершенно диких вещей, таких как полная оптимизация кода или вызывает сбои программы, как могло бы сделать неопределенное поведение. - person Lundin; 15.01.2020
comment
@Lundin: Да, необходимые точки последовательности существуют. Нет, утверждение «вызов функции вводит точку следования» является недостаточным утверждением этого. Ответ нуждался в более подробной информации в то время, когда я писал комментарий. С тех пор он был отредактирован. - person Eric Postpischil; 15.01.2020

Я попытался, и я обновил запись 0.

Однако, согласно этому вопросу: будет правая сторона выражение всегда вычисляется первым

Порядок оценки не определен и не определен. Поэтому я думаю, что такого кода следует избегать.

person Mickael B.    schedule 13.01.2020
comment
Поведение не неопределенное, а неопределенное. Естественно, в зависимости от того и другого следует избегать. - person Antti Haapala; 13.01.2020
comment
Хм, ах, и это не неупорядоченная, а неопределенно упорядоченная последовательность... 2 человека, случайно стоящих в очереди, имеют неопределенно упорядоченную последовательность. Нео внутри агента Смита не имеет последовательности, и произойдет неопределенное поведение. - person Antti Haapala; 13.01.2020

Поскольку нет большого смысла создавать код для присваивания до того, как у вас будет значение для присвоения, большинство компиляторов C сначала выдают код, который вызывает функцию, и сохраняют результат где-то (регистр, стек и т. д.), а затем они выдают код, который записывает это значение в его конечное место назначения, и поэтому они будут считывать глобальную переменную после ее изменения. Назовем это «естественным порядком», определяемым не каким-либо стандартом, а чистой логикой.

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

Таким образом, это в основном неопределенное поведение с очень плохим свойством, что вполне вероятно, что результат будет другим, в зависимости от того, выполняется ли оптимизация и насколько агрессивна эта оптимизация. Ваша задача как разработчика решить эту проблему с помощью кодирования:

int idx = global_var;
arr[idx] = update_three(2);

или кодирование:

int temp = update_three(2);
arr[global_var] = temp;

Хорошее эмпирическое правило: если глобальные переменные не являются const (или они не являются, но вы знаете, что никакой код никогда не изменит их как побочный эффект), вы никогда не должны использовать их непосредственно в коде, как в многопоточной среде. , даже это может быть неопределенным:

int result = global_var + (2 * global_var);
// Is not guaranteed to be equal to `3 * global_var`!

Поскольку компилятор может прочитать его дважды, а другой поток может изменить значение между двумя чтениями. Тем не менее, опять же, оптимизация определенно приведет к тому, что код будет читать его только один раз, поэтому вы снова можете получить разные результаты, которые теперь также зависят от времени другого потока. Таким образом, у вас будет намного меньше головной боли, если вы сохраните глобальные переменные во временной переменной стека перед использованием. Имейте в виду, что если компилятор считает, что это безопасно, он, скорее всего, оптимизирует даже это и вместо этого будет использовать глобальную переменную напрямую, поэтому, в конце концов, это может не повлиять на производительность или использование памяти.

(На всякий случай, если кто-то спросит, зачем делать x + 2 * x вместо 3 * x - на некоторых процессорах сложение выполняется сверхбыстро, как и умножение на степень два, поскольку компилятор преобразует их в битовые сдвиги (2 * x == x << 1), однако умножение с произвольными числами может быть очень медленным, поэтому вместо умножения на 3 вы получаете гораздо более быстрый код, сдвигая x на 1 и добавляя x к результату - и даже этот трюк выполняется современными компиляторами, если вы умножаете на 3 и включаете агрессивную оптимизацию, если это не современный целевой ЦП, где умножение выполняется так же быстро, как и сложение, поскольку тогда этот трюк замедлит вычисление.)

person Mecki    schedule 13.01.2020
comment
Это не неопределенное поведение - в стандарте перечислены возможности, и в любой момент выбирается одна из них. - person Antti Haapala; 13.01.2020
comment
Компилятор не будет превращать 3 * x в два чтения x. Он может прочитать x один раз, а затем выполнить метод x + 2*x для регистра, в который он считывает x. - person M.M; 13.01.2020
comment
@M.M Я никогда не говорил, что это читается 3 раза. Зачем ты придумываешь вещи? - person Mecki; 13.01.2020
comment
@AnttiHaapala Если вы не можете сказать, каков результат, просто взглянув на код, результат не определен, а определенное поведение приводит к определенным результатам. - person Mecki; 13.01.2020
comment
@ M.M Где я сказал, что компилятор превратит 3 * x в два чтения x? - person Mecki; 13.01.2020
comment
В последнем абзаце вы говорите, что компилятор может выполнить этот трюк, если включена агрессивная оптимизация, где трюк представлен как запись x + 2 * x вместо 3 * x - person M.M; 13.01.2020
comment
@ M.M Да, он выполняет трюк, я могу это доказать. Он сдвигается и добавляет, когда ваш код говорит умножить на три. Где я покажу вам вывод сборки? Я никогда не говорил, что он будет читать что-либо дважды, так как будет использовать регистр. Хитрость заключается в том, чтобы избежать умножения. - person Mecki; 13.01.2020
comment
Вы не упомянули, что он будет использовать регистр вместо двойного чтения, как указано в опубликованном вами коде, поэтому я добавил комментарий, чтобы указать, что он будет - person M.M; 13.01.2020
comment
@M.M Еще раз, чтобы даже вы могли это понять: я опубликовал пример кода, который позволяет компилятору C дважды читать глобальную переменную. На ЦП без регистров (например, ЦП на основе стека) это, скорее всего, также произойдет, поскольку нет регистров, а стек может быть таким же медленным, как и ОЗУ. Я также сказал, что при использовании оптимизации он, скорее всего, не будет читать его дважды, если этого можно избежать. И, наконец, в последнем абзаце я только объяснил, зачем вообще писать такой код, а не просто умножать на 3, и что даже компиляторы иногда сдвигают+добавляют вместо умножения. - person Mecki; 14.01.2020
comment
@Mecki Если вы не можете сказать, каков результат, просто взглянув на код, результат не определен - неопределенное поведение имеет очень специфическое значение в C/C++, и это не так. Другие ответчики объяснили, почему этот конкретный экземпляр unspecified, но не undefined. - person marcelm; 14.01.2020
comment
@marcelm Undefined имеет особое значение в английском языке, оно противоположно определению. Я не имел в виду какой-либо стандарт, не так ли? Я никогда не говорил, что это неопределенное поведение по какому-либо стандарту, не так ли? Я сказал, что поведение кода не определено (в этом смысл undefined), а если бы оно было определено, то и результат был бы определен. - person Mecki; 14.01.2020
comment
Я ценю намерение пролить свет на внутренности компьютера, даже если это выходит за рамки первоначального вопроса. Тем не менее, UB — это очень точный жаргон C/C++, и его следует использовать осторожно, особенно когда вопрос касается технических особенностей языка. Вместо этого вы можете рассмотреть возможность использования правильного неопределенного термина поведения, что значительно улучшит ответ. - person kuroi neko; 14.01.2020
comment
@Mecki: если части Стандарта и документация для реализации и ее среды выполнения вместе определяют поведение действия в некоторых конкретных обстоятельствах, но какая-то другая часть Стандарта классифицирует действие как Неопределенное поведение, если последнее будет награждено безусловным приоритет? - person supercat; 15.01.2020
comment
@Mecki Undefined имеет очень особое значение в английском языке... но в вопросе с пометкой language-lawyer, где рассматриваемый язык имеет свое собственное особое значение для undefined, вы только вызовете путаницу, не используя определение языка. - person TripeHound; 15.01.2020
comment
@TripeHound: По мнению авторов стандарта, поведение Undefined дает разработчику лицензию не обнаруживать определенные программные ошибки, которые трудно диагностировать. Он также определяет области возможного соответствующего языкового расширения: разработчик может дополнить язык, предоставив определение официально неопределенного поведения. Вопрос о том, какие популярные расширения следует поддерживать, является проблемой качества реализации за пределами юрисдикции. Стандарта. Авторы clang и gcc любят навязывать свое собственное значение, которое игнорирует заявленное намерение Комитета... - person supercat; 07.02.2020
comment
... что качественные реализации часто должны поддерживать популярные расширения, независимо от того, требует ли их Стандарт, но я думаю, что намерение вполне ясно из опубликованного Обоснования. - person supercat; 07.02.2020
comment
@supercat Я не думаю, что не согласен с тем, что вы написали, но я не уверен, почему вы обращаетесь ко мне ... мой комментарий был связан исключительно с желанием Меки использовать повседневное английское определение неопределенного поведения в ответ на вопрос, где UB имеет очень конкретное значение. - person TripeHound; 07.02.2020

Глобальное редактирование: извините, ребята, я весь загорелся и написал много ерунды. Просто старый чудак ругается.

Я хотел верить, что C пощадили, но, увы, с C11 его поставили на один уровень с C++. Очевидно, чтобы знать, что компилятор будет делать с побочными эффектами в выражениях, нужно теперь решить небольшую математическую загадку, связанную с частичным упорядочением кодовых последовательностей на основе "находится перед точкой синхронизации".

Мне посчастливилось спроектировать и внедрить несколько важных встроенных систем реального времени еще во времена K&R (включая контроллер электромобиля, который мог отправить людей врезаться в ближайшую стену, если двигатель не контролировался, 10-тонный промышленный робот, который мог бы раздавить людей в месиво, если бы ему не давали правильных команд, и системный уровень, который, хотя и безвредный, заставит несколько десятков процессоров высосать всю шину данных с менее чем 1% системной нагрузки).

Я могу быть слишком старым или глупым, чтобы понять разницу между undefined и unspecified, но я думаю, что у меня все еще есть довольно хорошее представление о том, что означают параллельное выполнение и доступ к данным. По моему, возможно, информированному мнению, эта одержимость парней из C++, а теперь и C с их любимыми языками, взявшими на себя проблемы синхронизации, является дорогостоящей несбыточной мечтой. Либо вы знаете, что такое параллельное выполнение, и вам не нужны никакие из этих штуковин, либо вы не знаете, и вы сделаете миру одолжение, не пытаясь с этим связываться.

Вся эта куча вызывающих слезы абстракций барьеров памяти просто связана с временным набором ограничений многопроцессорных систем кэширования, которые можно безопасно инкапсулировать в общие объекты синхронизации ОС, такие как, например, мьютексы и условные переменные C++. предложения.
Стоимость этой инкапсуляции — всего лишь минутное снижение производительности по сравнению с тем, чего можно достичь, используя детализированные конкретные инструкции ЦП, в некоторых случаях.
Ключевое слово volatile (или #pragma dont-mess-with-that-variable для всех I, как системный программист, будьте осторожны) было бы вполне достаточно, чтобы сказать компилятору прекратить переупорядочивать доступ к памяти. Оптимальный код можно легко создать с помощью прямых ассемблерных директив, чтобы дополнить низкоуровневый код драйвера и ОС специальными инструкциями для ЦП. Без глубоких знаний о том, как работает базовое оборудование (кэш-система или шинный интерфейс), вы все равно будете писать бесполезный, неэффективный или ошибочный код.

Небольшая корректировка ключевого слова volatile, и Боб стал бы кем угодно, кроме самого заядлого программиста низкого уровня. Вместо этого обычная банда фанатов математики C++ устроила полевой день, разрабатывая очередную непонятную абстракцию, поддавшись своей типичной склонности разрабатывать решения в поисках несуществующих проблем и путая определение языка программирования со спецификациями компилятора.

Только на этот раз изменение потребовало также исказить фундаментальный аспект C, поскольку эти «барьеры» должны были быть созданы даже в низкоуровневом коде C для правильной работы. Это, среди прочего, привело к хаосу в определении выражений без каких-либо объяснений или оправданий.

В заключение, тот факт, что компилятор может создать непротиворечивый машинный код из этого абсурдного куска C, является лишь отдаленным следствием того, как разработчики C++ справлялись с потенциальными несоответствиями систем кэширования в конце 2000-х годов.
Это произвело фурор. ужасный беспорядок в одном фундаментальном аспекте C (определение выражений), так что подавляющее большинство программистов на C, которым наплевать на системы кэширования, и это правильно, теперь вынуждены полагаться на гуру, чтобы объяснить разницу между a = b() + c() и a = b + c.

Попытка угадать, что станет с этим злополучным массивом, в любом случае является чистой потерей времени и усилий. Независимо от того, что компилятор сделает из этого, этот код патологически неверен. Единственная ответственная вещь, которую нужно сделать с ним, — это отправить его в корзину.
Концептуально побочные эффекты всегда можно убрать из выражений, приложив тривиальное усилие явного разрешения модификации до или после вычисления в отдельном операторе. .
Такой дерьмовый код мог быть оправдан в 80-х, когда нельзя было ожидать, что компилятор что-то оптимизирует. Но теперь, когда компиляторы давно стали умнее большинства программистов, все, что осталось, — это кусок дерьмового кода.

Я также не понимаю важности этой неопределенной/неопределенной дискуссии. Либо вы можете положиться на то, что компилятор сгенерирует код с согласованным поведением, либо нет. Называете ли вы это неопределенным или неопределенным, кажется спорным вопросом.

По моему, возможно, информированному мнению, C уже достаточно опасен в своем состоянии K&R. Полезной эволюцией было бы добавление мер безопасности, основанных на здравом смысле. Например, используя этот расширенный инструмент анализа кода, спецификации заставляют компилятор реализовывать, по крайней мере, генерацию предупреждений о сумасшедшем коде, вместо того, чтобы молча генерировать код, потенциально ненадежный до крайности.
Но вместо этого ребята решили, например, , чтобы определить фиксированный порядок вычислений в C++17. Теперь каждого недоумка-программиста активно подстрекают намеренно добавлять побочные эффекты в его/ее код, купаясь в уверенности, что новые компиляторы охотно справятся с запутыванием детерминированным образом.

K&R был одним из истинных чудес компьютерного мира. За двадцать баксов вы получаете исчерпывающую спецификацию языка (я видел, как отдельные люди пишут полные компиляторы, используя только эту книгу), отличное справочное руководство (оглавление обычно укажет вам на пару страниц ответа на ваш вопрос). вопрос), и учебник, который научит вас использовать язык осмысленно. В комплекте с обоснованиями, примерами и мудрыми словами предупреждения о многочисленных способах, которыми вы можете злоупотреблять языком, чтобы делать очень, очень глупые вещи.

Уничтожение этого наследия ради такой малой выгоды кажется мне жестокой растратой. Но, опять же, я вполне мог не понять сути. Может быть, какая-нибудь добрая душа подскажет мне пример нового кода на C, в котором используются значительные преимущества этих побочных эффектов?

person kuroi neko    schedule 15.01.2020
comment
Это неопределенное поведение, если есть побочные эффекты для одного и того же объекта в одном и том же выражении, C17 6.5/2. Они не секвенированы в соответствии с C17 6.5.18/3. Но текст из 6.5/2 Если побочный эффект на скалярном объекте не является последовательностью относительно другого побочного эффекта на том же скалярном объекте или вычисления значения с использованием значения того же скалярного объекта, поведение не определено. не применяется, поскольку вычисление значения внутри функции упорядочено либо до, либо после доступа к индексу массива, независимо от того, имеет ли оператор присваивания неупорядоченные операнды в себе. - person Lundin; 15.01.2020
comment
Вызов функции действует как мьютекс против непоследовательного доступа, если хотите. Подобно непонятному дерьму с оператором запятой, например 0,expr,0. - person Lundin; 15.01.2020
comment
Я думаю, вы поверили авторам Стандарта, когда они сказали, что Неопределенное поведение дает разработчику лицензию не обнаруживать определенные программные ошибки, которые трудно диагностировать. Он также определяет области возможного расширения соответствующего языка: разработчик может дополнить язык, предоставив определение официально неопределенного поведения. и сказал, что Стандарт не должен унижать полезные программы, которые не соответствуют строгому стандарту. Я думаю, что большинство авторов Стандарта сочло бы очевидным, что люди, стремящиеся писать качественные компиляторы... - person supercat; 07.02.2020
comment
... должны стремиться использовать UB как возможность сделать свои компиляторы максимально полезными для своих клиентов. Я сомневаюсь, что кто-либо мог предположить, что разработчики компиляторов будут использовать его как предлог для ответа на жалобы на то, что ваш компилятор обрабатывает этот код менее полезно, чем все остальные, потому что Стандарт не требует от нас полезной обработки, а реализации, которые обрабатывают с пользой программы, поведение которых не регламентируется Стандартом, просто способствуют написанию неработающих программ. - person supercat; 07.02.2020
comment
Я не вижу смысла в вашем замечании. Использование специфичного для компилятора поведения является гарантией непереносимости. Это также требует большой веры в производителя компилятора, который может в любой момент отменить любое из этих дополнительных определений. Единственное, что может сделать компилятор, — это генерировать предупреждения, которые мудрый и знающий программист может решить обрабатывать как ошибки. Проблема, которую я вижу с этим монстром ISO, заключается в том, что он делает такой ужасный код, как пример OP, законным (по крайне неясным причинам по сравнению с определением выражения K&R). - person kuroi neko; 07.02.2020