Dlaczego stwierdzenia bez skutku są uważane za legalne w C?

13

Wybacz, jeśli to pytanie jest naiwne. Rozważ następujący program:

#include <stdio.h>

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

W powyższym przykładzie, oświadczenia 5;i i;wydawać całkowicie zbędny, jednak kompiluje kod bez ostrzeżenia lub błędy domyślnie (jednakże gcc nie rzucać warning: statement with no effect [-Wunused-value]ostrzeżenie, gdy biegł z -Wall). Nie mają one wpływu na resztę programu, więc dlaczego są uważane za prawidłowe stwierdzenia? Czy kompilator po prostu je ignoruje? Czy są jakieś korzyści z dopuszczenia takich oświadczeń?

AW
źródło
5
Jakie są korzyści z banowania takich oświadczeń?
Mooing Duck
2
Każde wyrażenie może być stwierdzeniem, umieszczając ;po nim. To komplikuje język dodać więcej reguł, kiedy wyrażenia nie może być sprawozdanie
MM
3
Czy wolisz, aby Twój kod się nie skompilował, ponieważ ignorujesz zwracaną wartość printf()? Instrukcja w 5;zasadzie mówi „rób cokolwiek 5robi (nic) i ignoruj ​​wynik. Twoje zdanie printf(...)jest„ rób cokolwiek printf(...)robi i ignoruj ​​wyniki (wartość zwracana od printf()) ”. C traktuje to samo. Pozwala to również na kod taki jak (void) i;gdzie ijest parametr funkcji, którą przesyłasz, aby voidoznaczyć ją jako celowo nieużywaną
Andrew Henle
1
@AndrewHenle: To nie jest to samo, ponieważ wywoływanie printf()ma wpływ, nawet jeśli zignorujesz wartość, którą w końcu zwraca. Natomiast 5;nie ma żadnego efektu.
Nate Eldredge
1
Ponieważ Dennis Ritchie, a on nie jest w pobliżu, aby nam powiedzieć.
user207421

Odpowiedzi:

10

Jedną z zalet dopuszczenia takich instrukcji jest kod utworzony przez makra lub inne programy, a nie napisany przez ludzi.

Jako przykład wyobraź sobie funkcję, int do_stuff(void)która ma zwrócić 0 w przypadku sukcesu lub -1 w przypadku niepowodzenia. Może być tak, że obsługa „rzeczy” jest opcjonalna, więc możesz mieć plik nagłówkowy, który to robi

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

Teraz wyobraź sobie kod, który chce robić rzeczy, jeśli to możliwe, ale może tak naprawdę nie dbać o to, czy odniesie sukces, czy nie:

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();
}

Gdy STUFF_SUPPORTEDwynosi 0, preprocesor rozszerzy wywołanie func2do instrukcji, która właśnie czyta

    (-1);

tak więc w kompilatorze pojawi się „zbędne” stwierdzenie, które wydaje się niepokoić. Co jeszcze można zrobić? Jeśli ty #define do_stuff() // nothing, to kod func1się zepsuje. (I nadal będziesz mieć pustą instrukcję, func2która po prostu czyta ;, co jest być może nawet bardziej zbędne). Z drugiej strony, jeśli faktycznie musisz zdefiniować do_stuff()funkcję, która zwraca -1, możesz ponieść koszty wywołania funkcji bez powodu.

Nate Eldredge
źródło
Bardziej klasyczna wersja (czy mam na myśli zwykłą wersję) no-op to ((void)0).
Jonathan Leffler
Dobrym tego przykładem jest assert.
Neil
3

Proste instrukcje w C są zakończone średnikiem.

Proste instrukcje w C są wyrażeniami. Wyrażenie jest kombinacją zmiennych, stałych i operatorów. Każde wyrażenie powoduje pewną wartość określonego typu, którą można przypisać do zmiennej.

Powiedziawszy, że niektóre „inteligentne kompilatory” mogą odrzucić 5; i ja; sprawozdania.

J. Dumbass
źródło
Nie mogę sobie wyobrazić żadnego kompilatora, który zrobiłby cokolwiek z tymi stwierdzeniami oprócz ich odrzucenia. Co jeszcze może z nimi zrobić?
Jeremy Friesner
@JeremyFriesner: Bardzo prosty, nieoptymalizujący kompilator może równie dobrze wygenerować kod do obliczenia wartości i umieścić wynik w rejestrze (od tego momentu zostanie zignorowany).
Nate Eldredge
Standard C nie oznacza „prostej instrukcji”. Oświadczenie wyrażenie składa się z (opcjonalnie) wyrażenie a po nim średnik. Nie każde wyrażenie daje wartość; wyrażenie typu voidnie ma wartości.
Keith Thompson
2

Stwierdzenia bezskuteczne są dozwolone, ponieważ trudniej byłoby je zakazać niż zezwolić. Było to bardziej istotne, gdy C został zaprojektowany po raz pierwszy, a kompilatory były mniejsze i prostsze.

Oświadczenie wyrażenie składa się z wypowiedzi po średnikiem. Jego zachowanie polega na ocenie wyrażenia i odrzuceniu wyniku (jeśli istnieje). Zwykle celem jest to, że ocena wyrażenia ma skutki uboczne, ale nie zawsze jest łatwe lub nawet możliwe ustalenie, czy dane wyrażenie ma skutki uboczne.

Na przykład wywołanie funkcji jest wyrażeniem, więc wywołanie funkcji poprzedzone średnikiem jest instrukcją. Czy to stwierdzenie ma jakieś skutki uboczne?

some_function();

Nie można tego stwierdzić bez zobaczenia implementacji some_function.

Co powiesz na to?

obj;

Prawdopodobnie nie - ale jeśli obj jest zdefiniowane jako volatile, to robi.

Umożliwiając dowolny wyraz również osiągnąć w sposób ekspresyjny-oświadczeniu dodając średnik sprawia, że definicja język prostsze. Wymaganie, aby wyrażenie miało skutki uboczne, zwiększyłoby złożoność definicji języka i kompilatora. C opiera się na spójnym zestawie reguł (wywołania funkcji są wyrażeniami, przypisania są wyrażeniami, wyrażenie poprzedzone średnikiem jest instrukcją) i pozwala programistom robić to, co chcą, nie uniemożliwiając im robienia rzeczy, które mogą, ale nie muszą mieć sensu.

Keith Thompson
źródło
2

Wymienione instrukcje bez skutku są przykładami wyrażeń , których składnia jest podana w sekcji 6.8.3p1 standardu C w następujący sposób:

 wyrażenie-wyrażenie :
    wyrażenie opt  ;

Cała sekcja 6.5 poświęcona jest definicji wyrażenia, ale luźno mówiąc, wyrażenie składa się ze stałych i identyfikatorów powiązanych z operatorami. W szczególności wyrażenie może zawierać operator przypisania i może nie zawierać wywołania funkcji.

Tak więc każde wyrażenie poprzedzone średnikiem kwalifikuje się jako wyrażenie wyrażenia. W rzeczywistości każdy z tych wierszy kodu jest przykładem wyrażenia wyrażenia:

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

Niektóre operatory zawierają efekty uboczne, takie jak zestaw operatorów przypisania i operatorów inkrementacji / dekrementacji przed / po, a operator wywołania funkcji () może mieć efekt uboczny w zależności od tego, co robi dana funkcja. Nie ma jednak wymogu, aby jeden z operatorów musiał wywoływać efekt uboczny.

Oto inny przykład:

atoi("1");

Jest to wywoływanie funkcji i odrzucanie wyniku, podobnie jak wywołanie printfw twoim przykładzie, ale w przeciwieństwie printfdo samego wywołania funkcji nie ma efektu ubocznego.

dbush
źródło
1

Czasami takie stwierdzenia są bardzo przydatne:

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

    return x*x;
}

Lub gdy podręcznik referencyjny każe nam po prostu czytać rejestry, aby coś zarchiwizować - na przykład, aby wyczyścić lub ustawić flagę (bardzo powszechna sytuacja w świecie 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

P__J__
źródło
Kiedy *SREGjest lotny, *SREG;nie ma żadnego wpływu na model określony przez standard C. Norma C określa, że ​​ma zauważalny efekt uboczny.
Eric Postpischil
@EricPostpischil - nie, nie ma obserwowalnego efektu, ale jeśli ma efekt. Żaden z widocznych obiektów C nie uległ zmianie.
P__J__
C 2018 5.1.2.3 6 definiuje obserwowalne zachowanie programu w taki sposób, że „Dostęp do obiektów lotnych jest oceniany ściśle zgodnie z regułami maszyny abstrakcyjnej”. Nie ma mowy o interpretacji lub dedukcji; to jest definicja obserwowalnego zachowania.
Eric Postpischil