Czy przepełnienie liczby całkowitej ze znakiem nadal jest niezdefiniowane w C ++?

84

Jak wiemy, przepełnienie liczb całkowitych ze znakiem jest niezdefiniowanym zachowaniem . Ale w cstdintdokumentacji C ++ 11 jest coś interesującego :

typ liczby całkowitej ze znakiem o szerokości odpowiednio 8, 16, 32 i 64 bitów bez wypełniania bitów i przy użyciu dopełnienia 2 dla wartości ujemnych (podany tylko wtedy, gdy implementacja bezpośrednio obsługuje typ)

Zobacz link

I tu jest moje pytanie: skoro średnia mówi wyraźnie, że w przypadku int8_t, int16_t, int32_ta int64_tliczby ujemne są 2 uzupełniają, to nadal przepełnienia tych typów niezdefiniowany zachowanie?

Edycja Sprawdziłem standardy C ++ 11 i C11 i oto co znalazłem:

C ++ 11, §18.4.1:

Nagłówek definiuje wszystkie funkcje, typy i makra, tak samo jak 7.20 w standardzie C.

C11, §7.20.1.1:

Nazwa typedef intN_toznacza typ liczby całkowitej ze znakiem o szerokości N, bez bitów wypełniających i reprezentacją dopełnienia do dwójki. Zatem int8_toznacza taki typ liczby całkowitej ze znakiem o szerokości dokładnie 8 bitów.

Archie
źródło
14
Nigdy nie zapominaj, że jedyną podstawową dokumentacją C ++ jest standard. Wszystko inne, nawet wiki, takie jak CppReference, jest dodatkowym źródłem. To nie znaczy, że jest źle; po prostu nie do końca wiarygodne.
Nicol Bolas
Spodziewałbym się, że będzie to UB, nie ma wyjątku dla tych typów w C, nie rozumiem, dlaczego C ++ miałby je dodać.
Daniel Fischer
3
Jestem trochę zdezorientowany: gdzie jest czasownik w zdaniu „typ liczby całkowitej ze znakiem o szerokości dokładnie 8, 16, 32 i 64 bitów, bez bitów dopełniających i używając dopełnienia 2 dla wartości ujemnych (podany tylko wtedy, gdy implementacja obsługuje bezpośrednio Typ)?" Brakuje trochę? Co to znaczy?
YSC
C ++ 11 jest oparty na C99, a nie C11. Ale to i tak nie jest ważne
LF

Odpowiedzi:

81

czy nadal przepełnienie tych typów jest niezdefiniowanym zachowaniem?

Tak. Zgodnie z paragrafem 5/4 standardu C ++ 11 (w odniesieniu do dowolnego wyrażenia w ogóle):

Jeżeli podczas oceny wyrażenia wynik nie jest matematycznie zdefiniowany lub nie mieści się w zakresie reprezentowalnych wartości dla jego typu, zachowanie jest niezdefiniowane . […]

Fakt, że dla tych typów ze znakiem użyta jest reprezentacja dopełnienia do dwójki, nie oznacza, że ​​przy obliczaniu wyrażeń tych typów używany jest arytmetyczny modulo 2 ^ n.

Z drugiej strony, jeśli chodzi o arytmetykę bez znaku , standard wyraźnie określa, że ​​(paragraf 3.9.1 / 4):

Liczb całkowitych, stwierdził unsigned, powinny przestrzegać prawa arytmetyki modulo 2 ^ n , gdzie n jest liczbą bitów w reprezentacji wartości tej określonej wielkości całkowitej

Oznacza to, że wynik operacji arytmetycznej bez znaku jest zawsze „ zdefiniowany matematycznie ”, a wynik zawsze mieści się w reprezentowalnym zakresie; dlatego 5/4 nie ma zastosowania. W przypisie 46 wyjaśniono to:

46) Oznacza to, że arytmetyka bez znaku nie powoduje przepełnienia, ponieważ wynik, który nie może być reprezentowany przez wynikowy typ liczby całkowitej bez znaku, jest zmniejszany o modulo liczbę, która jest o jeden większa niż największa wartość, która może być reprezentowana przez wynikowy typ liczby całkowitej bez znaku.

Andy Prowl
źródło
1
Ten akapit oznaczałby również, że niepodpisane przepełnienie jest nieokreślone, a tak nie jest.
Archie
8
@Archie: Niezupełnie, ponieważ wartości bez znaku są definiowane modulo zakres bez znaku.
Wyścigi lekkości na orbicie
3
@Archie: Próbowałem wyjaśnić, ale zasadniczo otrzymałeś odpowiedź od LightnessRacesinOrbit
Andy Prowl
1
Właściwie nie ma znaczenia, czy przepełnienie bez znaku jest zdefiniowane, czy nie, jeśli nie może wystąpić z powodu obliczenia modulo ...
Aconcagua
1
Istnieją operacje bez znaku, których wynik nie jest „matematycznie zdefiniowany” - szczególnie dzielenie przez zero - więc być może twoje sformułowanie nie jest dokładnie tym, co miałeś na myśli w tym zdaniu. ITYM, że kiedy wynik jest zdefiniowany matematycznie , to jest również zdefiniowany w C ++.
Toby Speight
22

Tylko dlatego, że typ jest zdefiniowany do używania reprezentacji dopełnienia 2s, nie oznacza to, że przepełnienie arytmetyczne tego typu zostanie zdefiniowane.

Niezdefiniowane zachowanie przepełnienia arytmetycznego ze znakiem jest używane do włączania optymalizacji; na przykład kompilator może założyć, że jeśli a > bto a + 1 > brównież; nie dotyczy to arytmetyki bez znaku, w przypadku której należałoby przeprowadzić drugie sprawdzenie ze względu na możliwość, że a + 1może się to zakończyć 0. Ponadto niektóre platformy mogą generować sygnał pułapki przy przepełnieniu arytmetycznym (patrz np. Http://www.gnu.org/software/libc/manual/html_node/Program-Error-Signals.html ); norma nadal na to pozwala.

ecatmur
źródło
5
Warto zauważyć, że wiele osób bardziej "martwi się" możliwościami pułapek, ale założenia kompilatora są w rzeczywistości bardziej podstępne (jeden z powodów, dla których chciałbym, aby istniała kategoria pomiędzy zachowaniem zdefiniowanym w implementacji i niezdefiniowanym - w przeciwieństwie do zachowania zdefiniowanego w implementacji które wymagają określonych implementacji, aby coś zrobiły w spójny, udokumentowany sposób, chciałbym zachowania "ograniczonego implementacją", które wymagałoby implementacji, aby określić wszystko, co mogłoby się wydarzyć w wyniku czegoś (specyfikacje mogą jawnie obejmować niezdefiniowane zachowanie, ale ... .
SuperCat
3
... zachęca się, aby implementacje były bardziej szczegółowe, jeśli są praktyczne). Na sprzęcie, na którym liczby z dopełnieniem do dwóch byłyby naturalnie „zawijane”, nie ma sensownego powodu dla kodu, który chce, aby wynik w postaci zawiniętej liczby całkowitej wykonywał wiele instrukcji próbujących wykonać bez przepełnienia całkowitoliczbowego obliczenia, które sprzęt mógłby wykonać w jednej lub dwóch instrukcjach .
supercat
1
@supercat W rzeczywistości kod, który chce opakować wynik, może (na procesorach uzupełniających 2) po prostu rzutować operandy na odpowiednie typy bez znaku i wykonywać operację (a następnie rzutować z powrotem, uzyskując wartość zdefiniowaną w implementacji): działa to w przypadku dodawania, odejmowania i mnożenia . Jedynym problemem jest dzielenie, modulo i takie funkcje jak abs. Do tych operacji, gdy działa, nie wymaga więcej instrukcji niż w przypadku podpisanej artmetyki.
Ruslan
@Ruslan: W przypadkach, gdy kod wymaga precyzyjnie opakowanego wyniku, rzutowanie na bez znaku byłoby brzydkie, ale niekoniecznie generuje dodatkowy kod. Większym problemem byłby kod, który musi szybko zidentyfikować „potencjalnie interesujących” kandydatów, który spędza większość czasu na odrzucaniu nieciekawych kandydatów. Jeśli daje się kompilatorowi swobodę arbitralnego zachowania lub odrzucania dodatkowej precyzji z wartościami całkowitymi ze znakiem, ale wymaga, aby rzutowanie z powrotem do typu całkowitego obcinało taką precyzję, która umożliwi większość użytecznych optymalizacji, które można osiągnąć poprzez wykonanie przepełnienia UB , ...
supercat
... ale pozwoliłoby kodowi wymagającemu precyzyjnego opakowania na użycie jednego rzutowania zamiast dwóch (np. (int)(x+y)>zporównałby opakowany wynik), a także pozwoliłoby programistom pisać x+y>zw przypadkach, w których byłoby dopuszczalne, aby kod dawał 0 lub 1 w przypadku przepełnienia, pod warunkiem, że nie ma innych skutków ubocznych . Jeśli 0 lub 1 byłoby równie akceptowalnym wynikiem, pozwolenie programiście na napisanie tego zamiast jednego z nich (long)x+y>zlub (int)((unsigned)x+y)>zpozwoliłoby kompilatorom na wybranie jednej z ostatnich funkcji, która byłaby tańsza w danym kontekście [każda byłaby w niektórych przypadkach tańsza].
supercat
1

Tak bym się założył.

Ze standardowej dokumentacji (str. 4 i 5):

1.3.24 niezdefiniowane zachowanie

zachowanie, dla którego niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań

[Uwaga: można oczekiwać niezdefiniowanego zachowania, gdy w niniejszej Normie Międzynarodowej pomija się jakąkolwiek wyraźną definicję zachowania lub gdy program używa błędnej konstrukcji lub błędnych danych. Dopuszczalne niezdefiniowane zachowanie sięga od całkowitego zignorowania sytuacji z nieprzewidywalnymi skutkami, poprzez zachowanie podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z lub bez wydania komunikatu diagnostycznego), aż po przerwanie tłumaczenia lub wykonania (z wydaniem komunikatu diagnostycznego). Wiele błędnych konstrukcji programów nie powoduje nieokreślonego zachowania; wymagają diagnozy. - uwaga końcowa]

EnzoR
źródło