10.5 — Konwersje arytmetyczne

W lekcji 6.1 — Pierwszeństwo operatora i łączność, omówiliśmy, w jaki sposób wyrażenia są oceniane zgodnie z pierwszeństwem i łącznością ich operatorów.

Rozważmy następujące wyrażenie:

int x { 2 + 3 };

Binary operator+ podaje dwa operandy, oba typu int. Ponieważ oba operandy są tego samego typu, ten typ zostanie użyty do wykonania obliczeń, a zwrócona wartość również będzie tego samego typu. Zatem 2 + 3 otrzyma int wartości 5.

Ale co się stanie, gdy operandy operatora binarnego są różnych typów?

??? y { 2 + 3.5 };

W tym przypadku operator+ otrzymuje jeden argument typu int i drugi typu double. Czy wynik operatora powinien zostać zwrócony jako int, a double, czy może zupełnie inaczej?

W C++ niektóre operatory wymagają, aby ich operandy były tego samego typu. Jeśli jeden z tych operatorów zostanie wywołany z operandami różnych typów, jeden lub oba operandy zostaną niejawnie przekonwertowane na pasujące typy przy użyciu zestawu reguł zwanych zwykłymi konwersjami arytmetycznymi. Dopasowany typ powstały w wyniku zwykłych reguł konwersji arytmetycznej nazywany jest wspólnym typem operandów.

Operatory, które wymagają argumentów tego samego typu

Następujące operatory wymagają, aby ich operandy były tego samego typu:

  • Binarne operatory arytmetyczne: +, -, *, /, %
  • Binarne operatory relacyjne: <, >, <=, >=, ==, !=
  • Binarne bitowe operatory arytmetyczne: &, ^, |
  • Operator warunkowy ?: (z wyłączeniem warunku, który ma być typu bool)

Dla zaawansowanych czytelników

Przeciążone operatory nie podlegają zwykłej konwersji arytmetycznej reguły.

Zwykłe reguły konwersji arytmetycznej

Zwykłe reguły konwersji arytmetycznej są nieco złożone, więc nieco uprościmy. Kompilator ma rankingową listę typów, która wygląda mniej więcej tak:

  • long double (najwyższy stopień)
  • double
  • float
  • long long
  • long
  • int (najniższy stopień)

W celu znalezienia pasującego typu stosowane są następujące reguły:

Krok 1:

  • Jeśli jeden operand jest typem całkowitym, a drugi zmiennoprzecinkowym, operand całkowy jest konwertowany na typ operandu zmiennoprzecinkowego (nie ma potrzeby promocji całkowej miejsce).
  • W przeciwnym razie wszystkie argumenty całkowite są promowane numerycznie (patrz 10.2 -- Promocja zmiennoprzecinkowa i całkowa).

Krok 2:

  • Po awansowaniu, jeśli jeden operand jest ze znakiem, a drugi bez znaku, obowiązują specjalne zasady (patrz poniżej)
  • W przeciwnym razie konwertowany jest argument o niższej randze do typu operandu o wyższej randze.

Dla zaawansowanych czytelników

Specjalne zasady dopasowywania dla operandów całkowitych z różnymi znakami:

  • Jeśli ranga operandu bez znaku jest większa lub równa rangi operandu ze znakiem, operand ze znakiem jest konwertowany na typ operandu bez znaku.
  • Jeśli typ operandu ze znakiem może reprezentować wszystkie wartości typu operandu bez znaku, typ operandu bez znaku jest konwertowany na typ operandu ze znakiem.
  • W przeciwnym razie oba operandy są konwertowane na odpowiedni typ operandu ze znakiem.

Powiązana treść

Możesz znaleźć pełne zasady zwykłych konwersji arytmetycznych tutaj.

Kilka przykładów

W poniższych przykładach użyjemy operatora typeid (zawartego w nagłówku <typeinfo>), aby pokazać wynikowy typ wyrażenia.

Najpierw dodajmy int oraz a double:

#include <iostream>
#include <typeinfo> // for typeid()

int main()
{
    int i{ 2 };
    std::cout << typeid(i).name() << '\n'; // show us the name of the type for i

    double d{ 3.5 };
    std::cout << typeid(d).name() << '\n'; // show us the name of the type for d

    std::cout << typeid(i + d).name() << ' ' << i + d << '\n'; // show us the type of i + d

    return 0;
}

W tym przypadku operand double ma najwyższy priorytet, więc operand o niższym priorytecie (typu int) jest konwertowany na typ do double wartości 2.0. Następnie double wartości 2.0 i 3.5 są dodawane w celu uzyskania double wyniku 5.5.

Na komputerze autora wypisuje:

int
double
double 5.5

Zauważ, że Twój kompilator może wyświetlić coś nieco innego, ponieważ nazwy wyświetlane przez typeid.name() są specyficzne dla implementacji.

Dodajmy teraz dwie wartości type short:

#include <iostream>
#include <typeinfo> // for typeid()

int main()
{
    short a{ 4 };
    short b{ 5 };
    std::cout << typeid(a + b).name() << ' ' << a + b << '\n'; // show us the type of a + b

    return 0;
}

Ponieważ żaden operand nie pojawia się na liście priorytetów, oba operandy przechodzą integralną promocję do typu int. Wynik dodania dwóch ints jest int, jak można się spodziewać:

int 9

Problemy ze znakiem i bez znaku

Ta hierarchia priorytetów i reguły konwersji mogą powodować pewne problemy podczas mieszania wartości ze znakiem i bez znaku. Na przykład spójrz na następujący kod:

#include <iostream>
#include <typeinfo> // for typeid()

int main()
{
    std::cout << typeid(5u-10).name() << ' ' << 5u - 10 << '\n'; // 5u means treat 5 as an unsigned integer

    return 0;
}

Możesz oczekiwać, że wyrażenie 5u - 10 będzie oceniane jako -5 od 5 - 10 = -5. Ale oto, co faktycznie skutkuje:

unsigned int 4294967291

Ze względu na reguły konwersji, int operand jest konwertowany na unsigned int. A ponieważ wartość -5 jest poza zakresem unsigned int, otrzymujemy wynik, którego się nie spodziewamy.

Oto kolejny przykład pokazujący wynik sprzeczny z intuicją:

#include <iostream>

int main()
{
    std::cout << std::boolalpha << (-3 < 5u) << '\n';

    return 0;
}

Chociaż jest dla nas jasne, że 5 jest większy niż -3, gdy to wyrażenie ocenia, -3 jest konwertowany na duży unsigned int który jest większy niż 5. Zatem powyższe wypisuje false zamiast oczekiwanego wyniku true.

Jest to jeden z głównych powodów, dla których należy unikać liczb całkowitych bez znaku — gdy mieszasz je z liczbami całkowitymi ze znakiem w wyrażeniach arytmetycznych, ryzykujesz nieoczekiwanymi wynikami. A kompilator prawdopodobnie nawet nie wyświetli ostrzeżenia.

std::common_type i std::common_type_t

W przyszłych lekcjach napotkamy przypadki, w których przydatna będzie wiedza, jaki jest powszechny typ dwóch typów. std::common_type i przydatny alias typu std::common_type_t (oba zdefiniowane w nagłówku <type_traits>) może być użyty właśnie w tym celu.

Dla przykład std::common_type_t<int, double> zwraca typowy typ int i double, I std::common_type_t<unsigned int, long> zwraca typowy typ unsigned int i long.

Pokażemy przykład, gdzie jest to przydatne w lekcji 11.8 -- Szablony funkcji z wieloma typami szablonów.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
75 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze