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

В C99 есть f() + g() undefined или просто неопределено?

Раньше я думал, что на C99, даже если побочные эффекты функций f и g вмешиваются, и хотя выражение f() + g() не содержит точки последовательности, f и g содержат некоторые, поэтому поведение будет неуказанным: либо f() вызывается перед g(), либо g() до f().

Я больше не уверен. Что делать, если компилятор строит функции (которые компилятор может решить, даже если функции не объявлены inline), а затем переупорядочивает инструкции? Можно ли получить результат, отличный от двух предыдущих? Другими словами, это поведение undefined?

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

4b9b3361

Ответ 1

Выражение f() + g() содержит минимум 4 точки последовательности; один до вызова f() (после того, как все ноль его аргументов оценивается); один до вызова g() (после того, как все ноль его аргументов оценивается); один как вызов f() возвращается; и один, когда возвращается вызов g(). Кроме того, две точки последовательности, связанные с f(), встречаются либо до, либо после двух точек последовательности, связанных с g(). То, что вы не можете сказать, это то, какой порядок будут иметь точки последовательности, - то есть ли f-точки перед g-точками или наоборот.

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

Таким образом, последовательность, в которой оцениваются f() и g(), не определена. Но все остальное довольно чисто.


В комментарии supercat спрашивается:

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

Я считаю, что правило "как будто" применяется, и компилятор не получает дополнительной широты, чтобы опустить точки последовательности, потому что он использует явно inline функцию. Основная причина думать, что (слишком ленив искать точную формулировку в стандарте) заключается в том, что компилятору разрешено встроить или не встроить функцию в соответствии с ее правилами, но поведение программы не должно меняться (кроме производительность).

Кроме того, что можно сказать о последовательности (a(),b()) + (c(),d())? Возможно ли выполнить c() и/или d() между a() и b(), или для a() или b() для выполнения между c() и d()?

  • Ясно, что a выполняется перед b, а c выполняется до d. Я считаю, что возможно выполнение c и d между a и b, хотя довольно маловероятно, чтобы компилятор сгенерировал такой код; аналогично, а и b могут быть выполнены между c и d. И хотя я использовал "и" в "c и d", это может быть "или", то есть любая из этих последовательностей операции соответствует ограничениям:

    • Определенно разрешено
    • ABCD
    • CDAB
    • Возможно разрешено (сохраняет порядок ≺ b, c ≺ d)
    • ACBD
    • AcDb
    • CADB
    • CABD

     
    Я считаю, что он охватывает все возможные последовательности. Смотрите также чат между Джонатаном Леффлером и AnArrayOfFunctions - суть в том, что AnArrayOfFunctions не считает, возможно разрешенные "последовательности".

Если бы такая вещь была бы возможна, это означало бы значительную разницу между встроенными функциями и макросами.

Существуют значительные различия между встроенными функциями и макросами, но я не думаю, что порядок в выражении является одним из них. То есть любая из функций a, b, c или d может быть заменена макросом, и может произойти одно и то же секвенирование макрочастиц. Основное отличие, как мне кажется, в том, что с встроенными функциями есть гарантированные точки последовательности в вызовах функций - как указано в главном ответе, - а также в операторах запятой. С помощью макросов вы теряете функциональные точки последовательности. (Итак, может быть, это значительная разница...) Однако во многих отношениях проблема скорее напоминает вопросы о том, сколько ангелов могут танцевать на голове булавки - на практике это не очень важно. Если бы кто-то представил мне выражение (a(),b()) + (c(),d()) в обзоре кода, я бы сказал им переписать код, чтобы он дал понять:

a();
c();
x = b() + d();

И это предполагает, что на b() vs d() не существует требования к критическому упорядочиванию.

Ответ 2

См. Приложение C для списка точек последовательности. Функциональные вызовы (точка между всеми оцениваемыми аргументами и выполнение передачи функции) являются точками последовательности. Как вы сказали, это неуказано, какая функция вызывается сначала, но каждая из двух функций либо увидит все побочные эффекты другой, либо вообще ничего.

Ответ 3

@dmckee

Ну, это не помещается в комментарии, но вот что:

Сначала вы пишете правильный статический анализатор. "Исправить" в этом контексте означает, что он не останется безмолвным, если есть что-то сомнительное в отношении проанализированного кода, поэтому на этом этапе вы весело conflate undefined и неуказанные поведения. Они являются плохими и неприемлемыми в критическом коде, и вы правильно их предупреждаете обоим из них.

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

Итак, вы хотите выпустить одно предупреждение для

*p = x;
y = *p;

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

Итак, вы научите ваш анализатор предположить, что p является допустимым указателем, как только вы предупреждали об этом в первый раз в приведенном выше коде, чтобы вы не предупреждали об этом во второй раз. В более общем плане вы научитесь игнорировать значения (и пути выполнения), которые соответствуют тому, о чем вы уже предупреждали.

Затем вы понимаете, что не многие люди пишут критический код, поэтому вы делаете другие, легкие анализы для остальных из них, основываясь на результатах первоначального правильного анализа. Скажем, слайсер программы C.

И вы скажете "им": вам не нужно проверять все (возможно, часто ложные) тревоги, исходящие из первого анализа. Нарезанная программа ведет себя так же, как и исходная программа, если ни одна из них не запускается. Slicer производит программы, которые эквивалентны для критерия разреза для "определенных" путей выполнения.

И пользователи весело игнорируют сигналы тревоги и используют слайсер.

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

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

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

И это конец истории, которая определенно не вписывалась в комментарий. Извиняюсь перед тем, кто читает это далеко.