Powrót do strony głównej

GCC składa x/x do 1 przy dzieleniu przez zero

Artykuł omawia przypadek, gdy GCC składa x/x do stałej 1 nawet przy x=0 z powodu UB. Opisano mechanizm składania stałych, porównanie z MSVC, rozwiązanie z volatile i konsekwencje dla programowania.

x/x = 1 w GCC: pułapka składania stałych i UB
Advertisement 728x90

GCC skraca x/x do 1 nawet przy zerze: analiza UB i stałej redukcji

Programista spodziewał się, że program w C++ z wejściem x i wyjściem x / x ugrzęźnie przy x = 0, pokazując błąd dzielenia przez zero. Zamiast tego GCC wypisał 1 bez żadnych wyjątków. Przyczyną jest stała redukcja: kompilator przekształcił x / x w stałą wartość 1, ignorując rzeczywistą wartość x w czasie działania.

Kod do demonstracji:

#include <iostream>
int main()
{
    int x;
    std::cin >> x;
    std::cout << x / x << std::endl;
}

Przy x = 0 zamiast SIGFPE lub wyjątku — ciche wypisanie 1. Flagi takie jak -O0 nie zmieniają zachowania: redukcja działa już na poziomie podstawowym.

Google AdInline article slot

Mechanizm stałej redukcji w GCC

Kompilator stosuje tożsamość algebraiczną: x / x = 1 dla x ≠ 0. W kodzie maszynowym nie ma dzielenia — tylko mov wartości stałe 1. Jest to standardowa optymalizacja, ale bez sprawdzania warunku.

GCC rozumuje następująco: dzielenie przez zero to UB (undefined behavior) zgodnie ze standardem C++. Standard nie określa zachowania, więc kompilator zakłada, że x ≠ 0, i dokonuje redukcji wyrażenia.

Porównanie kompilatorów:

Google AdInline article slot
  • GCC/Clang: redukcja do 1, brak dzielenia.
  • MSVC: generuje instrukcję dzielenia, rzuca wyjątkiem przy x = 0.

Oba podejścia są zgodne ze standardem — UB daje kompilatorom swobodę działania.

Podstawa matematyczna: błąd skrócenia

Skracanie x w x / x jest dopuszczalne tylko gdy x ≠ 0. Kompilator narusza to regułę, podobnie jak szkolny błąd w "udowodnieniu" 1=2:

Powiedzmy, że a = b
Wtedy a² = ab
...
(a-b)(a+b) = b(a-b)  ← dzielenie przez (a-b)=0 ukryte
...
2 = 1

Błąd polega na skróceniu bez sprawdzenia mianownika. GCC powtarza ten błąd: zakłada x ≠ 0 na podstawie UB.

Google AdInline article slot

Możliwości UB dla kompilatora

Przy UB kompilator może:

  • Wygenerować dzielenie (wyjątek sprzętowy).
  • Usunąć kod jako niedostępną część.
  • Wstawić stałą.
  • Zignorować gałąź.

Zasada: "UB nie występuje". Kompilator optymalizuje zakładając brak UB, co zwiększa wydajność, ale zmniejsza przewidywalność.

Jak zmusić GCC do awarii

Użyj volatile, aby zabronić założeń:

#include <iostream>
int main()
{
    int tmp;
    std::cin >> tmp;
    volatile int x = tmp;
    std::cout << x / x << std::endl;
}

volatile blokuje redukcję: kompilator generuje rzeczywiste dzielenie, a x=0 powoduje SIGFPE.

Co ważne

  • Stała redukcja w GCC stosuje x/x=1 nawet przy x=0 w czasie działania dzięki UB.
  • volatile zapobiega optymalizacji, zapewniając oczekiwaną awarię.
  • Standard C++ daje kompilatorom pełną swobodę przy UB, MSVC jest bardziej matematycznie uczciwy.
  • Sprawdzaj kod przed użyciem w naukach: optymalizacje ukrywają UB.
  • UB to nie błąd — to wolność dla agresywnych transformacji.

Konsekwencje dla deweloperów

To zachowanie podkreśla ryzyko UB: kompilator nie sygnalizuje błędów, a cicho zmienia semantykę. Dla deweloperów średnio- i wysokiego poziomu lekcja brzmi: ostrożność przy wyrażeniach, gdzie możliwa jest wartość zero:

  • Unikaj x/x w kodzie ogólnym.
  • Testuj z przypadkami granicznymi na różnych kompilatorach.
  • Używaj volatile lub sprawdzeń if (x != 0) dla przewidywalności.

Standard balansuje wydajność i bezpieczeństwo poprzez UB. Dyskusja: czy warto upośledzić zasady dla dzielenia przez zero, uczynić je zdefiniowanym (np. trap)?

— Editorial Team

Advertisement 728x90

Czytaj dalej