Чому твердження без ефекту вважаються законними в С?


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). Вони не впливають на решту програми, тому чому в першу чергу їх вважають дійсними твердженнями? Чи компілятор їх просто ігнорує? Чи є якісь переваги для дозволу таких тверджень?


5
Які переваги заборони таких заяв?
Mooing Duck

2
Будь-який вираз може бути твердженням, ставлячи ;після нього. Це ускладнить мову, щоб додати більше правил про те, коли вирази не можуть бути висловлюваннями
ММ

3
Ви б хотіли, щоб ваш код не вдався зібрати, оскільки ви ігноруєте повернене значення printf()? У заяві в 5;основному говорить , «робити все , що 5робить (нічого) , і ігнорувати результат. Ваша заява printf(...)є" робити все , що printf(...)робить , і ігнорувати результати (значення, що повертається printf()) ". C лікує ті ж саме. Це також дозволяє коду , такі як , (void) i;де iзнаходиться параметр функції, яку ви призначили, voidщоб позначити її як навмисно невикористану.
Ендрю Генле,

1
@AndrewHenle: Це не зовсім те саме, тому що дзвінки printf()мають ефект, навіть якщо ви ігноруєте значення, яке воно врешті повертається. На відміну від цього 5;взагалі не має ефекту.
Нейт Елдредж

1
Тому що Денніс Річі, і він не навколо, щоб нам сказати.
користувач207421

Відповіді:


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_SUPPORTEDбуде 0, препроцесор розширить виклик func2на оператор, який щойно читає

    (-1);

і таким чином пропуск компілятора побачить саме такий "зайвий" вислів, який, здається, вас турбує. Але що ще можна зробити? Якщо ви #define do_stuff() // nothing, то код у func1зламається. (І ви все ще будете мати пусте заяву , в func2тому , що просто читає ;, що , можливо , ще більш зайвим.) З іншого боку, якщо у вас є насправді визначити do_stuff()функцію , яка повертає -1, ви можете понести витрати на виклик функції без поважних причин.


Більш класична версія (або я маю на увазі звичайну версію) но-оп ((void)0).
Джонатан Леффлер

Хороший приклад тому assert.
Ніл

3

Прості заяви в C закінчуються крапкою з комою.

Прості заяви в C - це вирази. Вираз - це поєднання змінних, констант та операторів. Кожен вираз призводить до деякого значення певного типу, яке можна присвоїти змінній.

Сказавши, що деякі "розумні компілятори" можуть відкинути 5; і я; заяви.


Я не уявляю жодного компілятора, який би нічого не робив із цими твердженнями, крім того, щоб відкинути їх. Що ще могло з ними зробити?
Джеремі Фріснер

@JeremyFriesner: Дуже простий, неоптимізуючий компілятор може дуже добре генерувати код для обчислення значення та введення результату в регістр (з цього моменту він буде ігнорований).
Nate Eldredge

Стандарт 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від виклику функції сам по собі не має побічного ефекту.


1

Іноді такі твердження дуже зручні:

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

    return x*x;
}

Або коли довідковий посібник каже нам просто прочитати регістри, щоб щось архівувати - наприклад, очистити або встановити якийсь прапор (дуже поширена ситуація в світі UC)

#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


Коли *SREGє летючим, *SREG;не робить ефекту в моделі, визначеній стандартом C. Стандарт C визначає, що він має помітний побічний ефект.
Eric Postpischil

@EricPostpischil - ні він не має спостережуваного ефекту, але якщо має ефект. Жоден із видимих ​​C об'єктів не змінився.
P__J__

C 2018 5.1.2.3 6 визначає спостережувану поведінку програми, включаючи, що "Доступ до летючих об'єктів оцінюється строго відповідно до правил абстрактної машини". Не виникає питання тлумачення чи дедукції; це визначення спостережуваної поведінки.
Eric Postpischil
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.