Отказ от ответственности: мой вопрос вообще не практичен, это скорее вопрос о двух кодах, которые якобы нарушают правила и в некоторой степени компилируются (с большим или меньшим количеством предупреждений/ошибок по мнению компилятора).
Насколько я знаю, следующий код действителен C99:
#include <stdio.h>
#include <stddef.h>
int main(void) {
size_t n = sizeof(int[printf("%s", "Hello")]);
}
Я понимаю, что sizeof не оценивает выражение, если оно не является VLA (поскольку оно имеет непостоянный размер).
Однако я не уверен, что этот фрагмент кода действителен C99:
#include <stdio.h>
int main(int argc, char** argv);
int main(int argc, char* argv[sizeof(int[printf("%s\n", "Hello") + main(0, NULL)])]) {}
Запустите его в Compiler Explorer
sizeof(some VLA)?int main(int, char**) и определение типа int main(int, char*[some size])?Я думаю, что создание массива размера sizeof(VLA) допустимо, я не знаю правила, которое сделало бы это недействительным.
Если я правильно помню, char*[some size] как параметр функции распадается на char**, но я не знаю, имеет ли он неправильную форму, потому что объявление/определение не полностью совпадают.
И я понятия не имею, допустим ли рекурсивный вызов main из его параметров или нет, я, очевидно, не нашел ни одного ресурса или примера такого кода. Кроме того, есть ли у main правила рекурсивных вызовов в параметрах, отличные от других функций?
Я предполагаю, что компилятор может не оптимизировать его (я думаю, это не UB), поэтому я также думаю, что переполнение стека произойдет всегда.
Цитаты из стандарта были бы очень признательны, большое спасибо.
@SoronelHaetir В C++ это запрещено. В C такого ограничения нет.
Объявление параметра как массива корректируется так, чтобы параметр стал указателем. При этом выражение размера массива отбрасывается, и стандарт C не содержит явного указания о том, вычисляется ли оно. GCC и Clang исторически расходились во мнениях по этому поводу. Компилятор может не оценить это.
Обратите внимание, что вы можете избежать бесконечной рекурсии, используя что-то вроде argc ? this : that, чтобы выбрать, выполнять ли дальнейший рекурсивный вызов или нет.
@EricPostpischil Что касается корректировки, компилятор должен, по крайней мере, сначала прийти к выводу, что массив является допустимым объявлением и что он не является неполным типом (ограничения объявления массива «Тип элемента не должен быть неполным или функциональным типом») и что он не является t иметь нулевой размер (объявление массива: «Если размер представляет собой выражение, которое не является целочисленным константным выражением... /--/ «в противном случае каждый раз, когда оно вычисляется, оно будет иметь значение больше нуля.») I' Я не уверен, как компилятор может сделать такой вывод, не оценивая выражение размера массива?
@Lundin: этого нет в разделе ограничений, поэтому компилятору не требуется его диагностировать. Поведение просто не определено, а это значит, что стандарт допускает любое поведение, в том числе не диагностировать нарушение и не оценивать выражение.
@EricPostpischil На самом деле ниже в том же абзаце, который я цитировал (семантика), говорится, что «если выражение размера является частью операнда оператора sizeof и изменение значения выражения размера не повлияет на результат оператора, это не указано, оценивается ли выражение размера». Это во многом так, благодаря настройке массива.
@Lundin: Это означает, что если у вас есть что-то вроде sizeof (char (*)[foo()]), foo() может быть оценено, а может и нет, потому что это не изменит результат выражения sizeof; это будет размер указателя на массив, независимо от размера массива. Здесь это не тот случай; значение, полученное в результате выражения sizeof, будет меняться в зависимости от размера массива.
@EricPostpischil Хм, ладно, да, так что это независимо от того, какой результат sizeof в данном случае передается во внешнее объявление размера массива. Тем не менее текст странный, поскольку подразумевает, что если значение влияет на результат оператора, то выражение вычисляется. Или зачем еще им это писать, если оно в любом случае не определено/не указано.





Допустимо ли иметь массив размером sizeof (некоторые VLA)?
Конечно. При объявлении VLA размер может быть любым выражением. Нет никаких причин, по которым sizeof выражения были бы запрещены.
char new_array[sizeof VLA];
эквивалентно
size_t n = sizeof VLA;
char new_array[n];
Допустимо ли объявление типа int main(int, char**) и определение типа int main(int, char*[some size])?
Да. В объявлении они эквивалентны.
И допустимо ли рекурсивно вызывать функцию из ее собственных параметров?
Функцию можно вызвать в любое время после объявления. Таким образом, вам просто нужно предварительное объявление, как и для функции, определенной после функции, в которой она вызывается.
Гарантировано ли, что рекурсия произойдет, или компилятор имеет право ее оптимизировать?
Оптимизатор может оптимизировать его, если определит, что это не изменит четко определенный результат. Если на самом деле рекурсия приведет к бесконечной рекурсии, и оптимизатор может это определить, он может оптимизировать все это, поскольку можно предположить, что не будет никакого неопределенного поведения.
«из своих параметров», как в примере в вопросе, а не как при использовании тех же аргументов.
@Barmar Спасибо за четкий ответ. Я отредактировал, чтобы задать еще один вопрос об оптимизации и о том, что разрешено делать компилятору, не могли бы вы ответить на этот вопрос? Кроме того, я задавался вопросом о рекурсии, потому что думал, что в параметрах функция на самом деле не существует, поэтому, возможно, будут применяться другие правила.
Оптимизатору разрешено делать все, что не меняет результат.
Ой, я неправильно понял, что вы имели в виду под «по своим параметрам». Я думал, вы имели в виду «использование собственных параметров»
Спасибо за редактирование, теперь оно отвечает на все вопросы.
Насколько мне известно, C99 не настаивает на завершении программ и, в частности, не запрещает неограниченную рекурсию. Могут быть программы, которые данная реализация C не может выполняться в соответствии со спецификацией, и некоторые из них обычно имеют глубокую или неограниченную рекурсию, но это не делает поведение таких программ неопределенным. Насколько мне известно, спецификация не лицензирует компиляторы для оптимизации бесконечной рекурсии.
Ответ: «Оптимизатор может оптимизировать его, если определит, что он не изменит четко определенный результат»: это либо неправильно, либо неполно, в зависимости от определения «четко определенного». Некоторые соответствующие программы имеют несколько разрешенных вариантов поведения, определенных стандартом C. Например, программы, содержащие вычисления с неопределенной последовательностью, могут иметь несколько разрешенных вариантов поведения. В некоторых программах рекурсию можно оптимизировать, даже если это меняет поведение: от одного разрешенного поведения к другому.
@EricPostpischil Если вы сможете придумать лучшее описание в одном предложении о том, когда разрешена оптимизация, я заменю его. В противном случае я не думаю, что здесь место для трактата по теории оптимизации.
@Barmar: Вопросы с тегами языковой юрист — это место для трактатов по оптимизации.
«Оптимизатор может оптимизировать ее, если наблюдаемое в результате поведение будет таким же, как и при неоптимизированной реализации программы».
@EricPostpischil Разве это не та же проблема, если есть несколько разрешенных вариантов поведения? Оптимизатор может переключать его с одного на другое.
@Barmar: Если существует несколько разрешенных вариантов поведения, то программа может быть реализована как программа A с одним поведением или как программа B с другим поведением или, возможно, с несколькими. Утверждение, что оптимизатор может что-то оптимизировать, если результирующее поведение такое же, как и в неоптимизированной реализации программы, позволяет сделать поведение таким же, как у реализации A или таким же, как у оптимизации B, и так далее.
- Допустимо ли иметь массив размером
sizeof(some VLA)?
Да.
Основными соответствующими положениями C99 были:
[...]
[и]могут ограничивать выражение или*. Если они ограничивать выражение (которое определяет размер массива), выражение должно иметь целочисленный тип. Если выражение является константой выражение, оно должно иметь значение больше нуля. [...]Обычный идентификатор (как определено в 6.2.3), который имеет переменное значение. модифицированный тип должен иметь область действия блока и не иметь связи или функции. объем прототипа. Если идентификатор объявлен как объект с статическая продолжительность хранения, она не должна иметь массив переменной длины тип.
(C99 6.7.5.2/1-2)
Обратите внимание, что единственное ограничение на структуру выражения, обозначающего размер массива, если оно присутствует, заключается в том, что оно имеет целочисленный тип. Контекст использования не предъявляет никаких других требований к задействованным операторам или операндам.
Если размер является целочисленным константным выражением и тип элемента имеет известный постоянный размер, тип массива не является массивом переменной длины тип; в противном случае тип массива является типом массива переменной длины.
Если размер представляет собой выражение, которое не является целочисленной константой выражение: если оно встречается в объявлении в области прототипа функции, он рассматривается так, как если бы он был заменен на *; в противном случае каждый раз оценено, оно должно иметь значение больше нуля. Размер каждого экземпляр типа массива переменной длины не изменяется во время его продолжительность жизни. Если выражение размера является частью операнда sizeof оператор и изменение значения выражения размера не приведет влияют на результат оператора, неизвестно, влияет ли вычисляется выражение размера.
(С99 6.7.5.2/4-5)
Обратите внимание, что последнее предложение не относится к вашему конкретному случаю; Я включил его для полноты картины, поскольку он предполагает смешивание sizeof и VLA.
Единственное дополнительное требование здесь — чтобы значение было больше 0, но это всегда верно для выражений sizeof (объектов нулевого размера не существует).
- Допустимо ли иметь объявление типа
int main(int, char**)и определение типаint main(int, char*[some size])?
Да.
Основными соответствующими положениями C99 были:
Все объявления в одной области действия, которые ссылаются на один и тот же объект или функция должна указывать совместимые типы.
(С99 6.7/4)
Обратите внимание, что предполагается несколько объявлений одной и той же функции.
Чтобы два типа функций были совместимыми, оба должны указывать совместимые типы возврата. Кроме того, список типов параметров, если они оба присутствуют, должны договориться о количестве параметров и использовании многоточия терминатор; соответствующие параметры должны иметь совместимые типы. [...] (При определении совместимости типов и составного типа, каждый параметр, объявленный с помощью функции или типа массива, принимается как имеющий настроенный тип и каждый параметр, объявленный с квалифицированным тип считается имеющим неполную версию объявленного типа.)
(С99 6.7.5.3/15)
Обратите внимание, в частности, что при сравнении типов параметров используется скорректированный тип. Это относится к
Объявление параметра как «массива типа» должно быть скорректировано до «полного указателя на тип», где квалификаторы типа (если таковые имеются) — это те, которые указаны в [ и ] вывода типа массива.
(С99 6.7.5.3/7)
Эта настройка определяет разницу между вашими char** и char*[some size], где они появляются как типы параметров функции.
- И допустимо ли рекурсивно вызывать функцию из ее собственных параметров?
Это зависит.
C99 не допускает вызовы необъявленных функций (хотя многие компиляторы принимают их как расширение обратной совместимости), а идентификатор функции не находится в области видимости до тех пор, пока не будет завершено ее объявление. Таким образом, выражение в объявлении функции не может вызвать эту функцию, если ему не предшествует другое объявление той же функции. Обычно это не так, но ваш пример позаботился об этом.
В противном случае, как уже обсуждалось, единственным ограничением формы выражения размера в VLA является то, что оно имеет целочисленный тип. Это не ограничивается контекстом, в котором появляется декларация VLA.
- Гарантировано ли, что рекурсия произойдет, или компилятор имеет право ее оптимизировать?
Нет.
Основным положением здесь является
Наименьшие требования к соответствующей реализации:
[...]
[...]
(С99 5.1.2.3/5)
Это основное положение о том, какие оптимизации могут быть выполнены в целом. Предположим, мы интерпретируем printf() для вывода в файл (и это обычная интерпретация), невозможно оптимизировать printf вызовы, которые вообще выдают какой-либо вывод. Однако компилятору разрешено преобразовать это, например, из рекурсии в цикл.
Если рекурсия произойдет, это гарантировано, что произойдет переполнение стека?
Нет.
Даже если компилятор реализует это как рекурсию, в спецификации языка ничего не говорится о стеке или его переполнении. Это твердо относится к области детализации реализации.
Более того, даже в реализации, которая использует стек вызовов и, следовательно, предположительно подвержена переполнению стека, компилятор может избежать этого в данном конкретном случае, заметив, что это хвостовая рекурсия и, следовательно, каждый рекурсивный вызов может повторно использовать один и тот же кадр стека.
Спасибо за подробности об оптимизации, я не знал, что эту рекурсию можно преобразовать в цикл.
@Chi_Iroh, любую рекурсию можно преобразовать в цикл и наоборот. Но в данном конкретном случае это преобразование довольно тривиально. Это не всегда так.
@Chi_Iroh Читать Лямбда: идеальный переход
@JohnBollinger Я понимаю, что не совсем ясно выразился, я имел в виду, что не знал, что оптимизатору разрешено преобразовывать рекурсию в цикл.
@Barmar Спасибо за ссылку, мне очень нравятся компьютеры и история программирования.
Я думаю, что ОП неявно спрашивает, могут ли или должны ли оцениваться выражения в прототипе функции. Я вижу, что ваш ответ явно не касается этого. Если мы согласны с тем, что они не оцениваются (согласно тексту, который вы цитируете из C99 6.7.5.2/4-5), то последующие вопросы о рекурсии являются спорными, поскольку в опубликованном коде рекурсия отсутствует.
@M.M Ты поднял хороший вопрос. Думаю, я не совсем ясно выразился, потому что второй код, который я написал в качестве примера, очень необычен и озадачивает меня. Если я правильно понимаю вашу точку зрения, выражение printf("hello") + main(0, NULL)' не должно оцениваться? Таким образом, GCC, clang и другие были бы неправы?
На самом деле это не хвостовая рекурсия. <expression> + <recursive call> нужно выполнить рекурсию, а затем сложить. Хотя можно преобразовать любую рекурсию в хвостовую рекурсию, это требует некоторого рефакторинга, и я бы не ожидал, что компилятор сделает это автоматически (подумайте о том, как бы вы переписали обычную factorial-функцию с хвостовой рекурсией).
@M.M, я понял, что вопрос «Я понимаю, что sizeof не оценивает выражение, если оно не является VLA (потому что оно имеет непостоянный размер)», отчасти означает, что OP признает, что оценивается операнд sizeof когда это VLA. Так что нет, я не думаю, что они об этом спрашивают.
@Barmar, OP main() всегда возвращает 0, поэтому программе не нужно выполнять рекурсию перед добавлением. Более того, значение выражения размера в любом случае фактически не используется из-за настройки параметров функции — его нужно только оценить на предмет побочных эффектов. Я думаю, что это не хвостовая рекурсия в соответствии с семантикой абстрактной машины, но нас это интересует только в контексте оптимизации, и оптимизатору не должно быть большого труда признать, что он может применять исключение хвостовых вызовов.
На самом деле, я бы совсем не удивился, если бы представление IL в GCC (например) было действительно хвостовой рекурсией.
@JohnBollinger Чтобы я мог с пользой ответить на ваш последний комментарий, можете ли вы явно указать, согласны ли вы с тем, что в коде, о котором спрашивают int main(int argc, char* argv[sizeof(int[printf("%s\n", "Hello") + main(0, NULL)])]) {}, выражение не оценивается? (Как определено в опубликованной вами стандартной цитате, C99 6.7.5.2/4-5).
Как перед корректировкой компилятор может сделать вывод, что объявление массива не является неполным типом или массивом нулевого размера (и то, и другое требуется деклараторами массива C17 6.7.6.2), если параметр размера не оценивается?
@M.M, это двусмысленность в спецификации: означает ли тот факт, что настройка типа параметра удаляет выражение, что оно не оценивается? Но общепринятая интерпретация (например, GCC) заключается в том, что оно оценивается. Это интерпретация, которую я предполагаю. Однако корректировка типа параметра означает, что результат оценки остается неиспользованным. Если бы оно вообще не было оценено, то на этом, конечно, дело было бы кончено.
Это адресовано мне, @Lundin? Я никогда не говорил, что выражение размера не оценивалось, и я думаю, из этого ответа достаточно ясно, что я интерпретирую спецификацию так, что оно оценивается. Я сказал, что результат не используется. А также то, что компилятор знает возвращаемое значение этого main() во время компиляции.
@Лундин. В 6.7.6.3/4 прямо сказано, что условие неполного типа проверяется после корректировки. Из стандарта мне неясно, происходит ли проверка размера VLA 0 до или после настройки, но это кажется спорным вопросом, поскольку поведение для размера 0 является UB, диагностика не требуется.
@M.M Да, но чтобы добраться до точки после корректировки, для начала это должно быть допустимое объявление массива, иначе его нельзя будет скорректировать. Таким образом, в первую очередь применяются части объявления массива — по той же причине, по которой мы не можем записывать параметры как int x[][]. Я не понимаю, почему компилятор блокирует это, но в то же время разрешает VLA, размер которого оценивается равным нулю. Очень может быть, что ни язык, ни существующие компиляторы здесь не имеют никакого смысла.
Правда, @Лундин, ты не можешь? Я думаю, ты недооцениваешь себя. int x[][] нарушает языковое ограничение, тогда как int x[n] с n неположительным вычислением во время выполнения нарушает (только) семантическое правило. Первый может быть обработан во время компиляции, и соответствующая реализация должна его диагностировать. Последний не может быть распознан до момента выполнения, и если реализация хочет утвердительно отклонить его, то для этого она должна создать инструментарий. Это, конечно, можно сделать, но я не понимаю, почему это происходит, когда на самом деле массив не выделяется.
Но на самом деле, @Lundin, я не думаю, что согласен с тем, что для настройки типа параметра требуется, чтобы размер VLA оценивался положительно. Ссылаясь на C17 (хотя C99 говорит то же самое), параграф 6.7.6.2/3 использует форму объявления, чтобы указать, что он объявляет массив. Отдельное семантическое правило (6.7.6.2/5) требует, чтобы выражение размера оценивалось положительно (если оно вообще оценивается), но это не спорно, не заменяет и даже не изменяет /3. И не следует этого делать, потому что, опять же, категория типа должна быть определена во время компиляции, а размерность переменной не может быть вычислена до времени выполнения.
Гарантировано ли, что рекурсия произойдет, или компилятор имеет право ее оптимизировать? Если произойдет рекурсия, гарантировано ли, что произойдет переполнение стека?
В спецификации есть пункт, который гласит:
Если размер представляет собой выражение, которое не является целочисленным константным выражением: если оно встречается в объявлении в области прототипа функции, оно рассматривается так, как если бы оно было заменено на
*
С99: 6.7.5.2/5
Это означает, что ваш пример кода эквивалентен
int main(int argc, char* argv[*])]) {}
и что выражение размера вообще не должно оцениваться. Однако кажется, что gcc этому не соответствует.
Область действия прототипа функции предназначена только для объявления функции, которое не является определением. int main(int argc, char* argv[*])]) {} не содержит ничего в области прототипа функции. Объявления его параметров находятся в области блока, связанной с телом основной функции, {}.
В стандарте C нет пункта 6.7.5.2.5. В пункте 6.7.2.5 есть абзац 5. Абзацы не следует цитировать так, как у вас, поскольку это не позволяет отличить п. 6.7.6.2 от п. 6.7.6 п. 2.
@EricPostpischil Правильный ли §6.7.5.2/5?
@Chi_Iroh: Это решает проблему двусмысленности. Использовать ли /, ¶, пробел или что-то еще — вопрос эстетики. Обычно я предпочитаю соответствовать оригинальному источнику в использовании круглых скобок, точек или других знаков, чтобы различать части документа. Стандарт C не использует никакой маркировки для различения абзацев, кроме положения на странице, фактически пробелы отделяются от номеров пунктов, поэтому я просто использую пробел, как в «6.7.2.5 5».
Понятно, спасибо за объяснение. Я только что отправил редактирование.
Я думаю, вы правы в точном определении «области прототипа функции», но тогда остается вопрос о том, когда и даже нужно ли оценивать выражение. Размеры VLA необходимы только при создании VLA или когда sizeof оценивается на VLA (без параметров), поэтому нет необходимости оценивать его для объявлений параметров функции.
Я бы подумал, что запрещенная часть вызывает main. Насколько я понял, рекурсивный вызов main фактически запрещен и не имеет никакого отношения к VLA.