W poprzedniej lekcji (10.3 -- Konwersje liczbowe), omówiliśmy konwersje numeryczne, które obejmują szeroki zakres konwersji różnych typów między podstawowymi typy.
Konwersje zawężające
W C++ instrukcja zawężająca konwersja to potencjalnie niebezpieczna konwersja liczbowa, w której typ docelowy może nie być w stanie pomieścić wszystkich wartości typu źródłowego.
Zawężające są następujące konwersje:
- Z typu zmiennoprzecinkowego na typ całkowity.
- Z typu zmiennoprzecinkowego na węższy lub niższy typ zmiennoprzecinkowy, chyba że konwertowana wartość to constexpr i mieści się w zakresie typu docelowego (nawet jeśli typ docelowy nie ma precyzji wystarczającej do przechowywania wszystkich znaczących cyfr liczby).
- Z typu całkowego na typ zmiennoprzecinkowy, chyba że konwertowana wartość to constexpr i której wartość można zapisać dokładnie w typie docelowym.
- Z typu całkowitego na inny typ całkowity, który nie może reprezentować wszystkich wartości typu pierwotnego, chyba że konwertowana wartość to constexpr i którego wartość można przechowywać dokładnie w typie docelowym. Obejmuje to zarówno szersze do węższych konwersje całkowite, jak i konwersje znaków całkowitych (ze znakiem na bez znaku lub odwrotnie).
W większości przypadków niejawne konwersje zawężające spowodują ostrzeżenia kompilatora, z wyjątkiem konwersji ze znakiem/bez znaku (które mogą, ale nie muszą, generować ostrzeżenia, w zależności od konfiguracji kompilatora).
Należy unikać konwersji zawężających, o ile to możliwe, ponieważ są one potencjalnie niebezpieczne, a tym samym źródło potencjalnych błędów.
Najlepsza praktyka
Ponieważ mogą być niebezpieczne i są źródłem błędów, w miarę możliwości unikaj konwersji zawężających.
Wyraźnie zaznacz celowe konwersje zawężające
Nie zawsze da się uniknąć konwersji zawężających - jest to szczególnie prawdziwe w przypadku wywołań funkcji, gdzie parametr funkcji i argument mogą mieć niedopasowany typ i wymagać konwersji zawężającej.
In w takich przypadkach dobrym pomysłem jest przekształcenie ukrytej konwersji zawężającej na jawną konwersję zawężającą za pomocą static_cast. Pomoże to udokumentować, że konwersja zawężająca jest zamierzona i pominie wszelkie ostrzeżenia kompilatora lub błędy, które w przeciwnym razie mogłyby wyniknąć.
Na przykład:
void someFcn(int i)
{
}
int main()
{
double d{ 5.0 };
someFcn(d); // bad: implicit narrowing conversion will generate compiler warning
// good: we're explicitly telling the compiler this narrowing conversion is intentional
someFcn(static_cast<int>(d)); // no warning generated
return 0;
}Najlepsza praktyka
Jeśli musisz wykonać konwersję zawężającą, użyj static_cast , aby przekonwertować ją na konwersję jawną.
Inicjalizacja nawiasów nie pozwala na konwersje zawężające
Konwersje zawężające są niedozwolone podczas korzystania z inicjalizacji nawiasów klamrowych (która jest jedną z główne powody, dla których preferowana jest ta forma inicjalizacji), a próba zrobienia tego spowoduje błąd kompilacji.
Na przykład:
int main()
{
int i { 3.5 }; // won't compile
return 0;
}Visual Studio generuje następujący błąd:
error C2397: conversion from 'double' to 'int' requires a narrowing conversion
Jeśli rzeczywiście chcesz wykonać zawężającą konwersję wewnątrz inicjalizacji nawiasów klamrowych, użyj static_cast , aby przekonwertować zawężającą konwersję na jawną konwersję:
int main()
{
double d { 3.5 };
// static_cast<int> converts double to int, initializes i with int result
int i { static_cast<int>(d) };
return 0;
}Niektóre konwersje constexpr nie są uważane za zawężające
Kiedy Wartość źródłowa konwersji zawężającej jest znana dopiero w czasie wykonywania, wyniku konwersji również nie można określić przed uruchomieniem. W takich przypadkach nie można określić, czy konwersja zawężająca zachowuje wartość, czy nie, aż do czasu wykonania. Na przykład:
#include <iostream>
void print(unsigned int u) // note: unsigned
{
std::cout << u << '\n';
}
int main()
{
std::cout << "Enter an integral value: ";
int n{};
std::cin >> n; // enter 5 or -5
print(n); // conversion to unsigned may or may not preserve value
return 0;
}W powyższym programie kompilator nie ma pojęcia, jaka wartość zostanie wpisana dla n. Podczas oceny print(n) zostanie wywołana konwersja z int Do unsigned int , a wyniki mogą zachować wartość lub nie, w zależności od tego, jaka wartość dla n została wprowadzona. Zatem kompilator, który ma włączone ostrzeżenia ze znakiem/niepodpisanym, wygeneruje ostrzeżenie w tym przypadku.
Być może jednak zauważyłeś, że większość definicji konwersji zawężających zawiera klauzulę wyjątku rozpoczynającą się od „chyba że konwertowana wartość to constexpr i…”. Na przykład konwersja zawęża się, gdy następuje „Z typu całkowitego do innego typu całkowitego, który nie może reprezentować wszystkich wartości typu pierwotnego, chyba że konwertowana wartość to constexpr i której wartość można przechowywać dokładnie w typie docelowym”.
Gdy wartością źródłową konwersji zawężającej jest constexpr, kompilator musi znać konkretną wartość do konwersji. W takich przypadkach kompilator może sam przeprowadzić konwersję, a następnie sprawdzić, czy wartość została zachowana. Jeżeli wartość nie została zachowana, kompilator może zatrzymać kompilację z powodu błędu. Jeśli wartość zostanie zachowana, konwersja nie jest uważana za zawężającą (a kompilator może zastąpić całą konwersję przekonwertowanym wynikiem, wiedząc, że jest to bezpieczne).
Na przykład:
#include <iostream>
int main()
{
constexpr int n1{ 5 }; // note: constexpr
unsigned int u1 { n1 }; // okay: conversion is not narrowing due to exclusion clause
constexpr int n2 { -5 }; // note: constexpr
unsigned int u2 { n2 }; // compile error: conversion is narrowing due to value change
return 0;
}Zastosujmy regułę „Z typu całkowitego do innego typu całkowitego, który nie może reprezentować wszystkich wartości typu pierwotnego, chyba że konwertowana wartość to constexpr i której wartość może być przechowywana dokładnie w typie docelowym” do obu powyższych konwersji.
W przypadku n1 i u1, n1 jest int i u1 jest unsigned int, więc jest to konwersja z typ całkowity na inny typ całkowity, który nie może reprezentować wszystkich wartości typu oryginalnego. Jednakże n1 jest constexpr, a jego wartość 5 można dokładnie przedstawić w typie docelowym (jako wartość bez znaku 5). Dlatego nie jest to uważane za konwersję zawężającą i możemy wyświetlić listę inicjalizacji u1 za pomocą n1.
W przypadku n2 i u2, sytuacja jest podobna. Chociaż n2 jest constexpr, jego wartość -5 nie może być dokładnie przedstawiona w typie docelowym, więc jest to uważane za konwersję zawężającą, a ponieważ wykonujemy inicjalizację listy, kompilator zgłosi błąd i zatrzyma kompilację.
Co dziwne, konwersje z typu zmiennoprzecinkowego na typ całkowity nie mają klauzuli wykluczającej constexpr, więc są one zawsze uważane za konwersje zawężające, nawet jeśli konwertowana wartość to constexpr i mieści się w zakresie typu docelowego:
int n { 5.0 }; // compile error: narrowing conversionCo jeszcze dziwniejsze, konwersje z typu zmiennoprzecinkowego constexpr na węższy typ zmiennoprzecinkowy nie są uważane za zawężające nawet w przypadku utraty precyzji!
constexpr double d { 0.1 };
float f { d }; // not narrowing, even though loss of precision resultsOstrzeżenie
Konwersja z typu zmiennoprzecinkowego constexpr na węższy typ zmiennoprzecinkowy nie jest uważana za zawężającą nawet w przypadku utraty precyzji.
Inicjalizacja listy za pomocą constexpr inicjalizatory
Te klauzule wyjątków constexpr są niezwykle przydatne podczas inicjowania listy obiektów innych niż int/non-double, ponieważ możemy użyć wartości inicjalizacji typu int lub double literał (lub obiektu constexpr).
Pozwala nam to uniknąć:
- konieczności używania sufiksów literału w większości przypadków
- konieczności zaśmiecania naszego inicjalizacje za pomocą static_cast
Na przykład:
int main()
{
// We can avoid literals with suffixes
unsigned int u { 5 }; // okay (we don't need to use `5u`)
float f { 1.5 }; // okay (we don't need to use `1.5f`)
// We can avoid static_casts
constexpr int n{ 5 };
double d { n }; // okay (we don't need a static_cast here)
short s { 5 }; // okay (there is no suffix for short, we don't need a static_cast here)
return 0;
}Działa to również z kopiowaniem i inicjalizacją bezpośrednią.
Warto wspomnieć o jednym zastrzeżeniu: inicjowanie typu zmiennoprzecinkowego o węższym lub niższym rankingu wartością constexpr jest dozwolone, o ile wartość mieści się w zakresie typu docelowego, nawet jeśli typ docelowy nie ma wystarczającej precyzji, aby precyzyjnie przechowywać wartość!
Kluczowa informacja
Typy zmiennoprzecinkowe są uszeregowane w tej kolejności (większy do mniejszego):
- Long double
- double
- Float
Dlatego coś takiego jest legalne i nie spowoduje wygenerowania błędu:
int main()
{
float f { 1.23456789 }; // not a narrowing conversion, even though precision lost!
return 0;
}Jednak twój kompilator może w takim przypadku wygenerować ostrzeżenie (tak robią GCC i Clang, jeśli użyjesz flagi kompilacji -Wconversion).

