>10.4 — Konwersje zawężające, inicjowanie list i inicjatory constexpr

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 conversion

Co 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 results

Ostrzeż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).

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:  
55 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze