Почему заявления без эффекта считаются законными в C?

13

Простите, если этот вопрос наивен. Рассмотрим следующую программу:

#include <stdio.h>

int main() {
  int i = 1;
  i = i + 2;
  5;
  i;
  printf("i: %d\n", i);
}

В приведенном выше примере, операторы 5;и i;кажутся совершенно излишними, но код компилируется без предупреждений или ошибок по умолчанию (однако, НКУ делает бросить warning: statement with no effect [-Wunused-value]предупреждение , когда бежал с -Wall). Они не влияют на остальную часть программы, так почему же они в первую очередь считаются действительными утверждениями? Компилятор просто игнорирует их? Есть ли какие-либо преимущества в разрешении таких заявлений?

AW
источник
5
Каковы преимущества запрета таких заявлений?
мычанка
2
Любое выражение может быть утверждением, поставив ;после него. Это усложнит язык, добавив больше правил, когда выражения не могут быть утверждениями
ММ
3
Вы бы предпочли, чтобы ваш код не компилировался, потому что вы игнорируете возвращаемое значение printf()? В заявлении в 5;основном говорит , «делать все , что 5не делает (ничего) , и игнорировать результат. Ваше заявление printf(...)является„делать все , что printf(...)делает , и игнорировать результаты (возвращаемое значение printf())“. C лечит те же самое. Это также позволяет кода , такие как , (void) i;где iнаходится параметр функции, которую вы приводите, чтобы voidпометить его как намеренно неиспользуемый
Эндрю Хенле
1
@AndrewHenle: Это не совсем то же самое, потому что вызов printf()имеет эффект, даже если вы игнорируете значение, которое он в конечном итоге возвращает. В отличие от 5;этого не имеет никакого эффекта вообще.
Нейт Элдридж
1
Потому что Денис Ричи, а он не рядом, чтобы рассказать нам.
user207421

Ответы:

10

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

В качестве примера представим функцию, int do_stuff(void)которая должна возвращать 0 в случае успеха или -1 в случае неудачи. Может случиться так, что поддержка «вещи» не является обязательной, и поэтому вы можете иметь заголовочный файл, который делает

#if STUFF_SUPPORTED
#define do_stuff() really_do_stuff()
#else
#define do_stuff() (-1)
#endif

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

void func1(void) {
    if (do_stuff() == -1) {
        printf("stuff did not work\n");
    }
}

void func2(void) {
    do_stuff(); // don't care if it works or not
    more_stuff();
}

Когда STUFF_SUPPORTED0, препроцессор расширит вызов func2до оператора, который просто читает

    (-1);

и поэтому проход компилятора увидит только то «лишнее» утверждение, которое вас беспокоит. Но что еще можно сделать? Если вы #define do_stuff() // nothing, то код в func1сломается. (И у вас все еще будет пустой оператор, func2который просто читает ;, что, возможно, еще более излишне.) С другой стороны, если вам действительно нужно определить do_stuff()функцию, которая возвращает -1, вы можете понести стоимость вызова функции без веской причины.

Нейт Элдридж
источник
Более классическая версия (или я имею в виду обычная версия) no-op есть ((void)0).
Джонатан Леффлер
Хороший пример этого assert.
Нил
3

Простые операторы в C заканчиваются точкой с запятой.

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

Сказав, что некоторые «умные компиляторы» могут отказаться от 5; и я; заявления.

Дж. Дамбасс
источник
Я не могу представить себе какой-либо компилятор, который будет делать что-либо с этими утверждениями, кроме как отбрасывать их. Что еще это может сделать с ними?
Джереми Фризнер
@JeremyFriesner: очень простой неоптимизирующий компилятор может сгенерировать код для вычисления значения и поместить результат в регистр (с этого момента он будет игнорироваться).
Нейт Элдридж
Стандарт C не использует термин «простое утверждение». Выражение утверждение состоит из ( по желанию) выражения следует точка с запятой. Не каждое выражение приводит к значению; выражение типа voidне имеет значения.
Кит Томпсон
2

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

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

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

some_function();

Невозможно сказать, не видя реализации some_function.

Как насчет этого?

obj;

Вероятно, нет - но если objопределяется как volatile, то это так.

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

Кит Томпсон
источник
2

Выражения , которые вы перечислили без эффекта, являются примерами выражения выражения , синтаксис которого приведен в разделе 6.8.3p1 стандарта C следующим образом:

 выражение-выражение :
    выражение opt  ;

Весь раздел 6.5 посвящен определению выражения, но условно говоря, выражение состоит из констант и идентификаторов, связанных с операторами. Примечательно, что выражение может содержать или не содержать оператор присваивания и может содержать или не содержать вызов функции.

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

i = i + 2;
5;
i;
printf("i: %d\n", i);

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

Вот еще один пример:

atoi("1");

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

dbush
источник
1

Иногда такие заявления очень удобны:

int foo(int x, int y, int z)
{
    (void)y;   //prevents warning
    (void)z;

    return x*x;
}

Или когда справочное руководство говорит нам просто читать регистры для архивирования чего-либо - например, для очистки или установки какого-либо флага (очень распространенная ситуация в мире ОК)

#define SREG   ((volatile uint32_t *)0x4000000)
#define DREG   ((volatile uint32_t *)0x4004000)

void readSREG(void)
{
    *SREG;   //we read it here
    *DREG;   // and here
}

https://godbolt.org/z/6wjh_5

P__J__
источник
Когда *SREGлетуч, *SREG;не не имеет никакого эффекта в модели , указанной в стандарте C. Стандарт C указывает, что он имеет наблюдаемый побочный эффект.
Эрик Постпишил
@EricPostpischil - нет, это не имеет наблюдаемого эффекта, но если имеет эффект. Ни один из видимых объектов C не изменился.
P__J__
C 2018 5.1.2.3 6 определяет наблюдаемое поведение программы, включающее следующее: «Доступ к изменчивым объектам оценивается строго в соответствии с правилами абстрактной машины». Нет вопроса о толковании или выводе; это определение наблюдаемого поведения.
Эрик Постпишил