Zawężanie konwersji w C ++ 0x. Czy to tylko ja, czy to brzmi jak przełomowa zmiana?


85

C ++ 0x spowoduje, że następujący i podobny kod będzie źle sformułowany, ponieważ wymaga tak zwanej konwersji zawężającej a doubledo a int.

int a[] = { 1.0 };

Zastanawiam się, czy ten rodzaj inicjalizacji jest często używany w kodzie świata rzeczywistego. Ile kodów zostanie uszkodzonych przez tę zmianę? Czy naprawienie tego w kodzie wymaga wiele wysiłku, jeśli w ogóle ma to wpływ na kod?


Dla odniesienia, patrz 8.5.4 / 6 n3225

Konwersja zawężająca jest konwersją niejawną

  • z typu zmiennoprzecinkowego na typ całkowity lub
  • od long double do double lub float, lub od double do float, z wyjątkiem sytuacji, gdy źródłem jest wyrażenie stałe, a rzeczywista wartość po konwersji mieści się w zakresie wartości, które można przedstawić (nawet jeśli nie można jej dokładnie przedstawić) lub
  • z typu całkowitego lub typu wyliczenia bez zakresu do typu zmiennoprzecinkowego, z wyjątkiem sytuacji, gdy źródło jest wyrażeniem stałym, a rzeczywista wartość po konwersji będzie pasować do typu docelowego i utworzy oryginalną wartość po przekonwertowaniu z powrotem na typ oryginalny lub
  • z typu całkowitego lub typu wyliczenia bez zakresu do typu całkowitego, który nie może reprezentować wszystkich wartości typu oryginalnego, z wyjątkiem sytuacji, gdy źródło jest wyrażeniem stałym, a rzeczywista wartość po konwersji będzie pasować do typu docelowego i wytworzy oryginalną wartość, gdy przekonwertowany z powrotem na oryginalny typ.

1
Zakładając, że dotyczy to tylko inicjalizacji typów wbudowanych, nie widzę, jak mogłoby to zaszkodzić. Jasne, może to spowodować uszkodzenie kodu. Ale powinno być łatwe do naprawienia.
Johan Kotlinski,

1
@John Dibling: Nie, inicjalizacja nie jest źle sformułowana, jeśli wartość może być dokładnie reprezentowana przez typ docelowy. (I 0tak już jest int.)
aschepler

2
@Nim: Zauważ, że jest to źle sformułowane tylko w {inicjatorach nawiasów klamrowych }, a jedyne starsze użycie ich dotyczy tablic i struktur POD. Ponadto, jeśli istniejący kod ma wyraźne rzutowania tam, gdzie należą, nie ulegnie awarii.
aschepler

4
@j_random_hacker, jak mówi dokument roboczy, int a = 1.0;jest nadal aktualny.
Johannes Schaub - litb

1
@litb: Dzięki. Właściwie uważam to za zrozumiałe, ale rozczarowujące - IMHO znacznie lepiej byłoby wymagać jawnej składni dla wszystkich zawężających konwersji od samego początku C ++.
j_random_hacker,

Odpowiedzi:


41

Wpadłem na tę przełomową zmianę, kiedy użyłem GCC. Kompilator wypisał błąd dla takiego kodu:

void foo(const unsigned long long &i)
{
    unsigned int a[2] = {i & 0xFFFFFFFF, i >> 32};
}

W funkcji void foo(const long long unsigned int&):

błąd: zawężanie konwersji (((long long unsigned int)i) & 4294967295ull)z long long unsigned intdo unsigned intwewnątrz {}

błąd: zawężanie konwersji (((long long unsigned int)i) >> 32)z long long unsigned intdo unsigned intwewnątrz {}

Na szczęście komunikaty o błędach były proste, a poprawka była prosta:

void foo(const unsigned long long &i)
{
    unsigned int a[2] = {static_cast<unsigned int>(i & 0xFFFFFFFF),
            static_cast<unsigned int>(i >> 32)};
}

Kod znajdował się w zewnętrznej bibliotece, a tylko dwa wystąpienia w jednym pliku. Nie sądzę, aby ta znacząca zmiana wpłynęła na znaczną część kodu. Nowicjusze mogą się mylić, choć.


9

Byłbym zaskoczony i rozczarowany, gdybym się dowiedział, że którykolwiek z kodów C ++, które napisałem w ciągu ostatnich 12 lat, miał tego rodzaju problem. Jednak większość kompilatorów przez cały czas wyrzucałaby ostrzeżenia o wszelkich „zawężeniach” w czasie kompilacji, chyba że czegoś mi brakuje.

Czy to również zawężające konwersje?

unsigned short b[] = { -1, INT_MAX };

Jeśli tak, myślę, że mogą pojawiać się nieco częściej niż przykład typu zmiennoprzecinkowego na typ całkowy.


1
Nie rozumiem, dlaczego twierdzisz, że byłoby to niezbyt częste do znalezienia w kodzie. Jaka jest logika między używaniem -1 lub INT_MAX zamiast USHRT_MAX? Czy USHRT_MAX nie osiągnął szczytów pod koniec 2010 roku?

7

Nie zdziwiłbym się, gdyby kogoś złapało coś takiego:

float ra[] = {0, CHAR_MAX, SHORT_MAX, INT_MAX, LONG_MAX};

(w mojej implementacji ostatnie dwa nie dają tego samego wyniku po przekonwertowaniu z powrotem na int / long, dlatego są zawężane)

Jednak nie pamiętam, bym kiedykolwiek to pisał. Jest to przydatne tylko wtedy, gdy przybliżenie granic jest do czegoś przydatne.

Wydaje się to również co najmniej niejasno prawdopodobne:

void some_function(int val1, int val2) {
    float asfloat[] = {val1, val2};    // not in C++0x
    double asdouble[] = {val1, val2};  // not in C++0x
    int asint[] = {val1, val2};        // OK
    // now do something with the arrays
}

ale nie jest to całkowicie przekonujące, ponieważ jeśli wiem, że mam dokładnie dwie wartości, po co umieszczać je w tablicach, a nie tylko float floatval1 = val1, floatval1 = val2;? Jaka jest jednak motywacja, dlaczego to powinno się kompilować (i działać, pod warunkiem, że utrata precyzji mieści się w dopuszczalnej dokładności dla programu), a float asfloat[] = {val1, val2};nie powinno? Tak czy inaczej, inicjalizuję dwa elementy zmiennoprzecinkowe z dwóch liczb całkowitych, po prostu w jednym przypadku te dwa elementy zmiennoprzecinkowe są członkami agregatu.

Wydaje się to szczególnie trudne w przypadkach, gdy wyrażenie niestałe powoduje zawężenie konwersji, mimo że (w określonej implementacji) wszystkie wartości typu źródłowego są reprezentowane w typie docelowym i konwertowane z powrotem do ich oryginalnych wartości:

char i = something();
static_assert(CHAR_BIT == 8);
double ra[] = {i}; // how is this worse than using a constant value?

Zakładając, że nie ma błędu, prawdopodobnie poprawka polega na tym, aby konwersja była jawna. Jeśli nie robisz czegoś dziwnego z makrami, myślę, że inicjator tablicy pojawia się tylko blisko typu tablicy lub przynajmniej do czegoś, co reprezentuje typ, co może zależeć od parametru szablonu. Więc rzut powinien być łatwy, jeśli jest szczegółowy.


8
„jeśli wiem, że mam dokładnie dwie wartości, po co umieszczać je w tablicach” - np. ponieważ wymaga tego API, takie jak OpenGL.
Georg Fritzsche,

7

Praktyczny przypadek, z którym się spotkałem:

float x = 4.2; // an input argument
float a[2] = {x-0.5, x+0.5};

Literał numeryczny jest niejawnie, doublektóry powoduje awans.


1
więc zrób to floatpisząc 0.5f. ;)
underscore_d

1
@underscore_d Nie działa, jeśli floatbył to typedef lub parametr szablonu (przynajmniej bez utraty precyzji), ale chodzi o to, że napisany kod działał z poprawną semantyką i stał się błędem w C ++ 11. To znaczy definicja „przełomowej zmiany”.
Jed

5

Spróbuj dodać -Wno-zawężanie do swoich CFLAGS, na przykład:

CFLAGS += -std=c++0x -Wno-narrowing

lub CPPFLAGS w przypadku kompilatorów C ++ (oczywiście zależy to od systemu kompilacji lub pliku Makefile)
Mikolasan

4

Zawężające błędy konwersji źle współdziałają z niejawnymi regułami promocji liczb całkowitych.

Wystąpił błąd z kodem, który wyglądał jak

struct char_t {
    char a;
}

void function(char c, char d) {
    char_t a = { c+d };
}

Co powoduje zawężający się błąd konwersji (który jest poprawny zgodnie ze standardem). Powodem jest to, że ci dniejawnie awansować do inti uzyskany intnie może być zawężony plecami do char w liście inicjatora.

OTOH

void function(char c, char d) {
    char a = c+d;
}

jest oczywiście nadal w porządku (w przeciwnym razie rozpętałoby się piekło). Ale, co zaskakujące, nawet

template<char c, char d>
void function() {
    char_t a = { c+d };
}

jest w porządku i kompiluje się bez ostrzeżenia, jeśli suma cid jest mniejsza niż CHAR_MAX. Nadal uważam, że jest to wada w C ++ 11, ale ludzie myślą inaczej - prawdopodobnie dlatego, że nie jest to łatwe do naprawienia bez pozbycia się niejawnej konwersji liczb całkowitych (która jest reliktem z przeszłości, kiedy ludzie pisali kod jak char a=b*c/di spodziewałem się, że zadziała, nawet jeśli (b * c)> CHAR_MAX) lub zawężenie błędów konwersji (które są prawdopodobnie dobrą rzeczą).


Wpadłem na następujący, który jest naprawdę irytujący nonsens: unsigned char x; static unsigned char const m = 0x7f; ... unsigned char r = { x & m };<- zawężająca konwersja wewnątrz {}. Naprawdę? Więc operator & również niejawnie konwertuje znaki bez znaku na int? Cóż, nie obchodzi mnie to, wynik jest nadal gwarantowany jako niepodpisany char, argh.
Carlo Wood

promocje „ niejawna konwersja liczb całkowitych ”?
curiousguy

2

To była rzeczywiście przełomowa zmiana, ponieważ doświadczenie z prawdziwego życia z tą funkcją pokazało, że gcc zmieniło zawężenie w ostrzeżenie o błędzie w wielu przypadkach z powodu prawdziwych problemów związanych z przenoszeniem baz kodu z C ++ 03 do C ++ 11. Zobacz ten komentarz w raporcie o błędzie gcc :

Norma wymaga jedynie, aby „implementacja zgodna z wymaganiami wystawiła co najmniej jeden komunikat diagnostyczny”, więc kompilowanie programu z ostrzeżeniem jest dozwolone. Jak powiedział Andrew, -Werror = narrowing pozwala uczynić to błędem, jeśli chcesz.

G ++ 4.6 dał błąd, ale został celowo zmieniony na ostrzeżenie dla 4.7, ponieważ wiele osób (łącznie ze mną) stwierdziło, że zawężanie konwersji było jednym z najczęściej napotykanych problemów podczas próby kompilacji dużych baz kodu C ++ 03 jako C ++ 11 . Wcześniej dobrze sformułowany kod, taki jak char c [] = {i, 0}; (gdzie zawsze znajdę się w zakresie znaków) powodowało błędy i musiałem zostać zmienione na char c [] = {(char) i, 0}


1

Wygląda na to, że GCC-4.7 nie wyświetla już błędów przy zawężaniu konwersji, ale zamiast tego wyświetla ostrzeżenia.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.