Porównania podpisane / niepodpisane

85

Próbuję zrozumieć, dlaczego poniższy kod nie generuje ostrzeżenia we wskazanym miejscu.

//from limits.h
#define UINT_MAX 0xffffffff /* maximum unsigned int value */
#define INT_MAX  2147483647 /* maximum (signed) int value */
            /* = 0x7fffffff */

int a = INT_MAX;
//_int64 a = INT_MAX; // makes all warnings go away
unsigned int b = UINT_MAX;
bool c = false;

if(a < b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a > b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a <= b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a >= b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a == b) // no warning <--- warning expected here
    c = true;
if(((unsigned int)a) == b) // no warning (as expected)
    c = true;
if(a == ((int)b)) // no warning (as expected)
    c = true;

Myślałem, że ma to związek z promocją w tle, ale dwie ostatnie wydają się mówić inaczej.

Moim zdaniem pierwsze ==porównanie jest tak samo niedopasowaniem ze znakiem / bez znaku, jak inne?

Piotr
źródło
3
gcc 4.4.2 wyświetla ostrzeżenie po wywołaniu z '-Wall'
bobah
To są spekulacje, ale być może optymalizuje wszystkie porównania, ponieważ zna odpowiedź w czasie kompilacji.
Null Set
2
Ach! re. Komentarz Bobah: Włączyłem wszystkie ostrzeżenia i teraz pojawia się brakujące ostrzeżenie. Uważam, że powinien był pojawić się na tym samym poziomie ostrzegawczym, co inne porównania.
Peter
1
@bobah: Naprawdę nienawidzę tego, że gcc 4.4.2 wyświetla to ostrzeżenie (bez możliwości nakazania mu, aby drukowało je tylko z powodu nierówności), ponieważ wszystkie sposoby wyciszenia tego ostrzeżenia pogarszają sytuację . Promocja domyślna niezawodnie konwertuje zarówno -1, jak i ~ 0 na najwyższą możliwą wartość dowolnego typu bez znaku, ale jeśli wyciszysz ostrzeżenie, rzucając je samodzielnie, musisz znać dokładny typ. Więc jeśli zmienisz typ (powiedzmy na unsigned long long), twoje porównania z gołymi -1będą nadal działać (ale te ostrzegają), podczas gdy twoje porównania z -1ulub (unsigned)-1oba zakończą się żałośnie.
Jan Hudec,
Nie wiem, dlaczego potrzebujesz ostrzeżenia i dlaczego kompilatory po prostu nie mogą sprawić, że zadziała. -1 jest ujemne, więc jest mniejsze niż jakakolwiek liczba bez znaku. Proste.
CashCow

Odpowiedzi:

95

Porównując podpisane z bez znaku, kompilator konwertuje podpisaną wartość na niepodpisaną. Dla równości to nie ma znaczenia -1 == (unsigned) -1. Dla innych porównań to ważne, na przykład poniższe nie jest prawdą -1 > 2U.

EDYCJA: Odnośniki:

5/9: (Wyrażenia)

Wiele operatorów binarnych, które oczekują operandów typu arytmetycznego lub wyliczeniowego, powoduje konwersje i zwraca typy wyników w podobny sposób. Celem jest uzyskanie wspólnego typu, będącego jednocześnie typem wyniku. Ten wzorzec nazywa się zwykłymi konwersjami arytmetycznymi, które są zdefiniowane w następujący sposób:

  • Jeśli jeden z operandów jest typu long double, drugi zostanie przekonwertowany na long double.

  • W przeciwnym razie, jeśli jeden z operandów jest podwójny, drugi zostanie przekonwertowany na podwójny.

  • W przeciwnym razie, jeśli jeden z operandów jest zmiennoprzecinkowy, drugi zostanie przekonwertowany na zmiennoprzecinkowy.

  • W przeciwnym razie promocje całkowe (4.5) będą wykonywane na obu operandach. 54)

  • Następnie, jeśli jeden z argumentów jest długi bez znaku, drugi zostanie przekonwertowany na długość bez znaku.

  • W przeciwnym razie, jeśli jeden operand jest typu long int, a drugi int bez znaku, to jeśli long int może reprezentować wszystkie wartości int unsigned int, to unsigned int zostanie przekonwertowany na int long int; w przeciwnym razie oba operandy zostaną przekonwertowane na unsigned long int.

  • W przeciwnym razie, jeśli jeden z operandów jest długi, drugi zostanie przekształcony w długi.

  • W przeciwnym razie, jeśli jeden z operandów jest bez znaku, drugi zostanie przekonwertowany na bez znaku.

4.7 / 2: (Integralne konwersje)

Jeśli typ docelowy jest bez znaku, wynikowa wartość jest najmniejszą liczbą całkowitą bez znaku zgodną ze źródłową liczbą całkowitą (modulo 2 n, gdzie n to liczba bitów użytych do reprezentacji typu bez znaku). [Uwaga: W reprezentacji dopełnienia do dwójki ta konwersja jest koncepcyjna i nie ma zmiany we wzorze bitowym (jeśli nie ma obcięcia). ]

EDIT2: poziomy ostrzegawcze MSVC

To, o czym ostrzega się na różnych poziomach ostrzegawczych MSVC, to oczywiście wybory dokonane przez programistów. Jak widzę, ich wybory dotyczące równości ze znakiem / bez znaku w porównaniu z większymi / mniejszymi porównaniami mają sens, jest to oczywiście całkowicie subiektywne:

-1 == -1oznacza to samo co -1 == (unsigned) -1- uważam, że wynik jest intuicyjny.

-1 < 2 to nie to samo, co -1 < (unsigned) 2- Na pierwszy rzut oka jest to mniej intuicyjne, a IMO zasługuje na „wcześniejsze” ostrzeżenie.

Erik
źródło
Jak przekonwertować podpisane na niepodpisane? Jaka jest wersja bez znaku wartości ze znakiem -1? (ze znakiem -1 = 1111, podczas gdy 15 bez znaku = 1111, bitowo mogą być równe, ale nie są logicznie równe.) Rozumiem, że jeśli wymusisz tę konwersję, zadziała, ale dlaczego kompilator miałby to zrobić? To nielogiczne. Co więcej, jak skomentowałem powyżej, kiedy włączyłem ostrzeżenia, pojawiło się brakujące ostrzeżenie ==, które wydaje się potwierdzać to, co mówię?
Peter,
1
Jak mówi 4.7 / 2, znak ze znakiem do unsigned oznacza brak zmiany wzoru bitowego dla uzupełnienia do dwóch. Jeśli chodzi o to, dlaczego kompilator to robi, jest to wymagane przez standard C ++. Uważam, że powodem ostrzeżeń VS na różnych poziomach jest możliwość niezamierzonego wyrażenia - zgadzam się z nimi, że porównanie równości znaków ze znakiem / bez znaku jest „mniej prawdopodobne”, że będzie problemem niż porównania nierówności. Jest to oczywiście subiektywne - takie wybory dokonali twórcy kompilatora VC.
Erik,
Ok, myślę, że prawie to rozumiem. Sposób, w jaki to przeczytałem, jest to, że kompilator (koncepcyjnie) robi: 'if (((unsigned _int64) 0x7fffffff) == ((unsigned _int64) 0xffffffff))', ponieważ _int64 jest najmniejszym typem, który może reprezentować zarówno 0x7fffffff, jak i 0xffffffff bez znaku?
Peter
2
Właściwie porównywanie (unsigned)-1lub -1uczęsto jest gorsze niż porównywanie -1. To dlatego (unsigned __int64)-1 == -1, ale (unsigned __int64)-1 != (unsigned)-1. Więc jeśli kompilator wyświetli ostrzeżenie, spróbujesz go wyciszyć przez rzutowanie na niepodpisane lub użycie, -1ua jeśli faktycznie zdarzy się, że wartość jest 64-bitowa lub zmienisz ją później na jedną, złamiesz swój kod! I pamiętaj, że size_tjest to wersja bez znaku, 64-bitowa tylko na platformach 64-bitowych, a używanie -1 dla nieprawidłowej wartości jest bardzo częste.
Jan Hudec,
1
Może cpmpilers nie powinien tego wtedy robić. Jeśli porównuje ze znakiem i bez znaku, po prostu sprawdź, czy wartość ze znakiem jest ujemna. Jeśli tak, to na pewno będzie mniejsza niż liczba bez znaku.
CashCow
32

Dlaczego podpisane / niepodpisane ostrzeżenia są ważne i programiści muszą zwracać na nie uwagę, przedstawia poniższy przykład.

Zgadnij wynik tego kodu?

#include <iostream>

int main() {
        int i = -1;
        unsigned int j = 1;
        if ( i < j ) 
            std::cout << " i is less than j";
        else
            std::cout << " i is greater than j";

        return 0;
}

Wynik:

i is greater than j

Zaskoczony? Demo online: http://www.ideone.com/5iCxY

Dolna linia: dla porównania, jeśli jeden operand jest unsigned, to drugi operand jest niejawnie konwertowany na, unsigned jeśli jego typ jest podpisany!

Nawaz
źródło
2
On ma rację! To głupie, ale on ma rację. To jest poważna wpadka, której nigdy wcześniej nie spotkałem. Dlaczego nie konwertuje wartości bez znaku na (większą) wartość ze znakiem ?! Jeśli zrobisz "if (i <((int) j))" to działa tak, jak byś się spodziewał. Chociaż "if (i <((_int64) j))" miałoby więcej sensu (zakładając, że nie możesz, że _int64 jest dwukrotnie większy od int).
Peter,
6
@Peter "Dlaczego nie konwertuje nieznajomego na (większą) wartość ze znakiem?" Odpowiedź jest prosta: może nie być większej wartości ze znakiem. Na maszynie 32-bitowej w dawnych czasach zarówno int, jak i long były 32-bitowe i nie było nic większego. Porównując podpisane i niepodpisane, najwcześniejsze kompilatory C ++ przekonwertowały oba na podpisane. Zapomniałem bowiem z jakich powodów komitet normalizacyjny C to zmienił. Najlepszym rozwiązaniem jest unikanie braku znaku tak bardzo, jak to możliwe.
James Kanze,
5
@JamesKanze: Podejrzewam, że ma to również coś wspólnego z faktem, że wynikiem przepełnienia ze znakiem jest Undefined Behavior, podczas gdy wynikiem przepełnienia bez znaku nie jest i dlatego konwersja wartości ujemnej ze znakiem na wartość bez znaku jest definiowana podczas konwersji dużej wartości bez znaku na wartość ujemną ze znakiem wartość nie jest .
Jan Hudec,
2
@James Kompilator zawsze mógł wygenerować zestaw, który implementowałby bardziej intuicyjną semantykę tego porównania bez rzutowania na jakiś większy typ. W tym konkretnym przykładzie wystarczyłoby najpierw sprawdzić, czy i<0. Wtedy ijest mniejszy niż jna pewno. Jeśli ijest nie mniejsza niż zero, ìmożna ją bezpiecznie przekonwertować na bez znaku, aby ją porównać j. Jasne, porównania między znakiem i bez znaku byłyby wolniejsze, ale ich wynik byłby w pewnym sensie bardziej poprawny.
Sven
@ Nawet się zgadzam. Standard mógł wymagać, aby porównania działały dla wszystkich rzeczywistych wartości, zamiast konwertowania ich na jeden z dwóch typów. To zadziałałoby jednak tylko w przypadku porównań; Podejrzewam, że komisja nie chciała innych reguł porównań i innych operacji (i nie chciała zaatakować problemu określania porównań, gdy faktycznie porównywany typ nie istnieje).
James Kanze
4

Operator == wykonuje po prostu bitowe porównanie (przez proste dzielenie, aby sprawdzić, czy wynosi 0).

Mniejsze / większe niż porównania opierają się znacznie bardziej na znaku liczby.

4-bitowy przykład:

1111 = 15? lub -1?

więc jeśli masz 1111 <0001 ... to niejednoznaczne ...

ale jeśli masz 1111 == 1111 ... To jest to samo, chociaż nie chciałeś, żeby tak było.

Yochai Timmer
źródło
Rozumiem to, ale to nie odpowiada na moje pytanie. Jak zauważyłeś, 1111! = 1111, jeśli znaki nie pasują. Kompilator wie, że istnieje niezgodność typów, więc dlaczego o tym nie ostrzega? (Chodzi mi o to, że mój kod może zawierać wiele takich niedopasowań, przed którymi nie jestem ostrzegany.)
Peter
Tak to jest zaprojektowane. Test równości sprawdza podobieństwo. I jest podobnie. Zgadzam się z tobą, że nie powinno tak być. Możesz zrobić makro lub coś, co przeciąża x == y być! ((X <y) || (x> y))
Yochai Timmer
1

W systemie, który reprezentuje wartości przy użyciu dopełnienia 2 (większość nowoczesnych procesorów), są one równe nawet w postaci binarnej. Może dlatego kompilator nie narzeka na a == b .

A dla mnie dziwne, że kompilator nie ostrzega o a == ((int) b) . Myślę, że powinno dać ci ostrzeżenie o obcięciu liczby całkowitej lub coś w tym rodzaju.

Hossein
źródło
1
Filozofia C / C ++ jest taka: kompilator ufa, że ​​programista wie, co robi podczas jawnej konwersji między typami. Tak więc nie ma ostrzeżenia (przynajmniej domyślnie - uważam, że istnieją kompilatory, które generują ostrzeżenia, jeśli poziom ostrzeżenia jest wyższy niż domyślny).
Péter Török
0

Podany wiersz kodu nie generuje ostrzeżenia C4018, ponieważ firma Microsoft użyła innego numeru ostrzeżenia (tj. C4389 ) do obsługi tego przypadku, a C4389 nie jest domyślnie włączona (tj. Na poziomie 3).

Z dokumentów firmy Microsoft dotyczących C4389:

// C4389.cpp
// compile with: /W4
#pragma warning(default: 4389)

int main()
{
   int a = 9;
   unsigned int b = 10;
   if (a == b)   // C4389
      return 0;
   else
      return 0;
};

Inne odpowiedzi całkiem dobrze wyjaśniły, dlaczego Microsoft mógł zdecydować się na specjalny przypadek z operatora równości, ale uważam, że te odpowiedzi nie są zbyt pomocne bez wspominania o C4389 lub o tym, jak włączyć to w Visual Studio .

Powinienem również wspomnieć, że jeśli zamierzasz włączyć C4389, możesz również rozważyć włączenie C4388. Niestety nie ma oficjalnej dokumentacji dla C4388, ale wydaje się, że pojawia się w wyrażeniach takich jak następujące:

int a = 9;
unsigned int b = 10;
bool equal = (a == b); // C4388
Tim Rae
źródło