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.
Johannes Schaub - litb
źródło
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ć.

Tymoteusza003
źródło
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.

aschepler
źródło
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.

Steve Jessop
źródło
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.

Jed
źródło
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
Kukuh Indrayana
źródło
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ą).

Gunther Piez
źródło
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}

Shafik Yaghmour
źródło
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.

kyku
źródło