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.

