Niespójne obcięcie wyrażeń liczb całkowitych bez znaku między C ++ i C w różnych kompilatorach

10

Edycja 2 :

Debugowałem dziwny błąd testu, gdy funkcja wcześniej znajdująca się w pliku źródłowym C ++, ale przeniesiona do pliku C dosłownie, zaczęła zwracać nieprawidłowe wyniki. Poniższe MVE pozwala odtworzyć problem z GCC. Jednak gdy kaprysem skompilowałem przykład z Clangiem (a później z VS), otrzymałem inny wynik! Nie mogę ustalić, czy traktować to jako błąd w jednym z kompilatorów, czy jako przejaw niezdefiniowanego wyniku dozwolonego przez standard C lub C ++. O dziwo, żaden z kompilatorów nie dał mi żadnych ostrzeżeń o tym wyrażeniu.

Sprawcą jest to wyrażenie:

ctl.b.p52 << 12;

Tutaj p52jest wpisany jako uint64_t; jest także częścią związku (patrz control_tponiżej). Operacja przesunięcia nie powoduje utraty żadnych danych, ponieważ wynik nadal mieści się w 64 bitach. Jednak wtedy GCC decyduje się skrócić wynik do 52 bitów, jeśli użyję kompilatora C. ! Kompilator C ++ zachowuje wszystkie 64 bity wyniku.

Aby to zilustrować, przykładowy program poniżej kompiluje dwie funkcje z identycznymi ciałami, a następnie porównuje ich wyniki. c_behavior()jest umieszczony w pliku źródłowym C i cpp_behavior()C ++ orazmain() dokonuje porównania.

Repozytorium z przykładowym kodem: https://github.com/grigory-rechistov/c-cpp-bitfields

Nagłówek common.h definiuje połączenie 64-bitowych pól bitowych i liczb całkowitych oraz deklaruje dwie funkcje:

#ifndef COMMON_H
#define COMMON_H

#include <stdint.h>

typedef union control {
        uint64_t q;
        struct {
                uint64_t a: 1;
                uint64_t b: 1;
                uint64_t c: 1;
                uint64_t d: 1;
                uint64_t e: 1;
                uint64_t f: 1;
                uint64_t g: 4;
                uint64_t h: 1;
                uint64_t i: 1;
                uint64_t p52: 52;
        } b;
} control_t;

#ifdef __cplusplus
extern "C" {
#endif

uint64_t cpp_behavior(control_t ctl);
uint64_t c_behavior(control_t ctl);

#ifdef __cplusplus
}
#endif

#endif // COMMON_H

Funkcje mają identyczne treści, z wyjątkiem tego, że jedna jest traktowana jako C, a druga jako C ++.

c-part.c:

#include <stdint.h>
#include "common.h"
uint64_t c_behavior(control_t ctl) {
    return ctl.b.p52 << 12;
}

cpp-part.cpp:

#include <stdint.h>
#include "common.h"
uint64_t cpp_behavior(control_t ctl) {
    return ctl.b.p52 << 12;
}

main.c:

#include <stdio.h>
#include "common.h"

int main() {
    control_t ctl;
    ctl.q = 0xfffffffd80236000ull;

    uint64_t c_res = c_behavior(ctl);
    uint64_t cpp_res = cpp_behavior(ctl);
    const char *announce = c_res == cpp_res? "C == C++" : "OMG C != C++";
    printf("%s\n", announce);

    return c_res == cpp_res? 0: 1;
}

GCC pokazuje różnicę między wynikami, które zwracają:

$ gcc -Wpedantic main.c c-part.c cpp-part.cpp

$ ./a.exe
OMG C != C++

Jednak z Clang C i C ++ zachowują się identycznie i zgodnie z oczekiwaniami:

$ clang -Wpedantic main.c c-part.c cpp-part.cpp

$ ./a.exe
C == C++

W Visual Studio otrzymuję taki sam wynik jak w przypadku Clanga:

C:\Users\user\Documents>cl main.c c-part.c cpp-part.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24234.1 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

main.c
c-part.c
Generating Code...
Compiling...
cpp-part.cpp
Generating Code...
Microsoft (R) Incremental Linker Version 14.00.24234.1
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:main.exe
main.obj
c-part.obj
cpp-part.obj

C:\Users\user\Documents>main.exe
C == C++

Próbowałem przykładów w systemie Windows, mimo że pierwotny problem z GCC został wykryty w systemie Linux.

Grigorij Rechistow
źródło
1
pola bitowe są notorycznie fałszywe dla dużych szerokości.
Natrafiłem
@chqrlie Przeczytałem operatora C<< jako wymagającego obcięcia.
Andrew Henle
Proszę zamieścić stackoverflow.com/help/minimal-reproducible-example . Obecny kod nie ma main.ci prawdopodobnie powoduje niezdefiniowane zachowanie na kilka sposobów. IMO łatwiej byłoby opublikować MRE z jednym plikiem, który generuje inne dane wyjściowe po skompilowaniu z każdym kompilatorem. Ponieważ interop C-C ++ nie jest dobrze określony przez standard. Zauważ też, że aliasing unii powoduje UB w C ++.
MM
@MM Tak, poślizgnęło się, gdy publikowałem pytanie. Dodałem go teraz i myślę, że posiadanie małego repozytorium może być również pomysłem
Grigorij Rechistov
@MM „IMO łatwiej byłoby opublikować MRE z jednym plikiem, który generuje różne dane wyjściowe po kompilacji z każdym kompilatorem.” Nie myślałem o tym, ponieważ przekształcałem mój kod produkcyjny w coś mniejszego, ale powinno być możliwe przeformułuj odtwarzacz w jeden plik.
Grigorij Rechistov

Odpowiedzi:

6

C i C ++ traktują typy elementów pola bitowego inaczej.

C 2018 6.7.2.1 10 mówi:

Pole bitowe jest interpretowane jako posiadające podpisaną lub niepodpisaną liczbę całkowitą składającą się z określonej liczby bitów…

Zauważ, że nie jest to specyficzne dla typu - jest to typ liczb całkowitych - i nie mówi, że typ jest typem użytym do zadeklarowania pola bitowego, jak uint64_t a : 1;pokazano w pytaniu. Widocznie pozostawia to wybór implementacji do implementacji.

Projekt C ++ 2017 n4659 12.2.4 [class.bit] 1 mówi o deklaracji pola bitowego:

… Atrybut pola bitowego nie jest częścią typu członka klasy…

Oznacza to, że w deklaracji, takich jak uint64_t a : 1;The : 1nie jest częścią rodzaju członka klasy a, więc typ to tak, jakby to było uint64_t a;, a tym samym typem ajest uint64_t.

Wygląda więc na to, że GCC traktuje pole bitowe w C jako jakąś liczbę całkowitą 32-bitową lub węższą, jeśli pasuje, a pole bitowe w C ++ jako deklarowany typ, co nie wydaje się naruszać standardów.

Eric Postpischil
źródło
Przeczytałem obcięcie w C jako obowiązkowe zgodnie z 6.5.7 4 (brzmienie C18 jest podobne): „Wynik E1 << E2 to przesunięte w lewo E1 pozycje bitów E2; puste bity są wypełniane zerami. Jeśli E1 ma typ bez znaku , wartość wyniku to E1 x 2E2, moduł zmniejszony o jeden więcej niż maksymalna wartość reprezentowana w typie wyniku. ” E1w tym przypadku jest to 52-bitowe pole bitowe.
Andrew Henle
@AndrewHenle: Rozumiem, co mówisz. Typem n -bitowego pola bitowego jest „ n -bitowa liczba całkowita” (na razie pomijamy sygnaturę). Interpretowałem to, ponieważ n- bitowe pole bitowe jest jakimś typem całkowitym wybranym przez implementację. Opierając się wyłącznie na sformułowaniu w 6.7.2.1 10, popieram twoją interpretację. Jednak problem z tym jest to, że, biorąc pod uwagę uint64_t a : 33zestaw do 2 ^ 33-1 w strukturze s, a następnie, w implementacji C z 32-bitowym int, s.a+s.apowinien dawać 2 ^ 33-2 powodu opakowania, ale Clang wywołuje 2 ^ 34- 2; najwyraźniej traktuje to jako uint64_t.
Eric Postpischil
@AndrewHenle: (Więcej na temat rozumowania: W s.a+s.azwykłe konwersje arytmetyczne nie zmieniłyby typu s.a, ponieważ jest szerszy niż unsigned int, więc arytmetyka byłaby wykonywana w typie 33-bitowym.)
Eric Postpischil
ale Clang produkuje 2 ^ 34-2; najwyraźniej traktuje to jako uint64_t. Jeśli jest to kompilacja 64-bitowa, wydaje się, że Clang jest spójny z tym, jak GCC traktuje kompilacje 64-bitowe, nie obcinając. Czy Clang traktuje kompilacje 32- i 64-bitowe inaczej? (I wygląda na to, że właśnie nauczyłem się innego powodu, aby unikać pól bitowych ...)
Andrew Henle,
@AndrewHenle: No, stary Jabłko Clang 1.7 produkuje 2 ^ 32-2 (nie 2 ^ 33-2; stracił trochę!) Zarówno -m32i -m64, z ostrzeżeniem, że typ jest rozszerzeniem GCC. W Apple Clang 11.0 nie mam bibliotek do uruchamiania kodu 32-bitowego, ale wygenerowany zestaw pokazuje pushl $3i pushl $-2przed wywołaniem printf, więc myślę, że jest to 2 ^ 34-2. Tak więc Apple Clang nie różni się między celami 32- i 64-bitowymi, ale z czasem się zmienił.
Eric Postpischil
4

Andrew Henle zasugerował ścisłą interpretację standardu C: typ pola bitowego jest liczbą całkowitą ze znakiem lub bez znaku o dokładnie określonej szerokości.

Oto test, który obsługuje tę interpretację: używając _Generic()konstrukcji C1x próbuję określić typ pól bitowych o różnych szerokościach. Musiałem zdefiniować je za pomocą typu, long long intaby uniknąć ostrzeżeń podczas kompilacji z clang.

Oto źródło:

#include <stdint.h>
#include <stdio.h>

#define typeof(X)  _Generic((X),                         \
                       long double: "long double",       \
                       double: "double",                 \
                       float: "float",                   \
                       unsigned long long int: "unsigned long long int",  \
                       long long int: "long long int",   \
                       unsigned long int: "unsigned long int",  \
                       long int: "long int",             \
                       unsigned int: "unsigned int",     \
                       int: "int",                       \
                       unsigned short: "unsigned short", \
                       short: "short",                   \
                       unsigned char: "unsigned char",   \
                       signed char: "signed char",       \
                       char: "char",                     \
                       _Bool: "_Bool",                   \
                       __int128_t: "__int128_t",         \
                       __uint128_t: "__uint128_t",       \
                       default: "other")

#define stype long long int
#define utype unsigned long long int

struct s {
    stype s1 : 1;
    stype s2 : 2;
    stype s3 : 3;
    stype s4 : 4;
    stype s5 : 5;
    stype s6 : 6;
    stype s7 : 7;
    stype s8 : 8;
    stype s9 : 9;
    stype s10 : 10;
    stype s11 : 11;
    stype s12 : 12;
    stype s13 : 13;
    stype s14 : 14;
    stype s15 : 15;
    stype s16 : 16;
    stype s17 : 17;
    stype s18 : 18;
    stype s19 : 19;
    stype s20 : 20;
    stype s21 : 21;
    stype s22 : 22;
    stype s23 : 23;
    stype s24 : 24;
    stype s25 : 25;
    stype s26 : 26;
    stype s27 : 27;
    stype s28 : 28;
    stype s29 : 29;
    stype s30 : 30;
    stype s31 : 31;
    stype s32 : 32;
    stype s33 : 33;
    stype s34 : 34;
    stype s35 : 35;
    stype s36 : 36;
    stype s37 : 37;
    stype s38 : 38;
    stype s39 : 39;
    stype s40 : 40;
    stype s41 : 41;
    stype s42 : 42;
    stype s43 : 43;
    stype s44 : 44;
    stype s45 : 45;
    stype s46 : 46;
    stype s47 : 47;
    stype s48 : 48;
    stype s49 : 49;
    stype s50 : 50;
    stype s51 : 51;
    stype s52 : 52;
    stype s53 : 53;
    stype s54 : 54;
    stype s55 : 55;
    stype s56 : 56;
    stype s57 : 57;
    stype s58 : 58;
    stype s59 : 59;
    stype s60 : 60;
    stype s61 : 61;
    stype s62 : 62;
    stype s63 : 63;
    stype s64 : 64;

    utype u1 : 1;
    utype u2 : 2;
    utype u3 : 3;
    utype u4 : 4;
    utype u5 : 5;
    utype u6 : 6;
    utype u7 : 7;
    utype u8 : 8;
    utype u9 : 9;
    utype u10 : 10;
    utype u11 : 11;
    utype u12 : 12;
    utype u13 : 13;
    utype u14 : 14;
    utype u15 : 15;
    utype u16 : 16;
    utype u17 : 17;
    utype u18 : 18;
    utype u19 : 19;
    utype u20 : 20;
    utype u21 : 21;
    utype u22 : 22;
    utype u23 : 23;
    utype u24 : 24;
    utype u25 : 25;
    utype u26 : 26;
    utype u27 : 27;
    utype u28 : 28;
    utype u29 : 29;
    utype u30 : 30;
    utype u31 : 31;
    utype u32 : 32;
    utype u33 : 33;
    utype u34 : 34;
    utype u35 : 35;
    utype u36 : 36;
    utype u37 : 37;
    utype u38 : 38;
    utype u39 : 39;
    utype u40 : 40;
    utype u41 : 41;
    utype u42 : 42;
    utype u43 : 43;
    utype u44 : 44;
    utype u45 : 45;
    utype u46 : 46;
    utype u47 : 47;
    utype u48 : 48;
    utype u49 : 49;
    utype u50 : 50;
    utype u51 : 51;
    utype u52 : 52;
    utype u53 : 53;
    utype u54 : 54;
    utype u55 : 55;
    utype u56 : 56;
    utype u57 : 57;
    utype u58 : 58;
    utype u59 : 59;
    utype u60 : 60;
    utype u61 : 61;
    utype u62 : 62;
    utype u63 : 63;
    utype u64 : 64;
} x;

int main(void) {
#define X(v)  printf("typeof(" #v "): %s\n", typeof(v))
    X(x.s1);
    X(x.s2);
    X(x.s3);
    X(x.s4);
    X(x.s5);
    X(x.s6);
    X(x.s7);
    X(x.s8);
    X(x.s9);
    X(x.s10);
    X(x.s11);
    X(x.s12);
    X(x.s13);
    X(x.s14);
    X(x.s15);
    X(x.s16);
    X(x.s17);
    X(x.s18);
    X(x.s19);
    X(x.s20);
    X(x.s21);
    X(x.s22);
    X(x.s23);
    X(x.s24);
    X(x.s25);
    X(x.s26);
    X(x.s27);
    X(x.s28);
    X(x.s29);
    X(x.s30);
    X(x.s31);
    X(x.s32);
    X(x.s33);
    X(x.s34);
    X(x.s35);
    X(x.s36);
    X(x.s37);
    X(x.s38);
    X(x.s39);
    X(x.s40);
    X(x.s41);
    X(x.s42);
    X(x.s43);
    X(x.s44);
    X(x.s45);
    X(x.s46);
    X(x.s47);
    X(x.s48);
    X(x.s49);
    X(x.s50);
    X(x.s51);
    X(x.s52);
    X(x.s53);
    X(x.s54);
    X(x.s55);
    X(x.s56);
    X(x.s57);
    X(x.s58);
    X(x.s59);
    X(x.s60);
    X(x.s61);
    X(x.s62);
    X(x.s63);
    X(x.s64);

    X(x.u1);
    X(x.u2);
    X(x.u3);
    X(x.u4);
    X(x.u5);
    X(x.u6);
    X(x.u7);
    X(x.u8);
    X(x.u9);
    X(x.u10);
    X(x.u11);
    X(x.u12);
    X(x.u13);
    X(x.u14);
    X(x.u15);
    X(x.u16);
    X(x.u17);
    X(x.u18);
    X(x.u19);
    X(x.u20);
    X(x.u21);
    X(x.u22);
    X(x.u23);
    X(x.u24);
    X(x.u25);
    X(x.u26);
    X(x.u27);
    X(x.u28);
    X(x.u29);
    X(x.u30);
    X(x.u31);
    X(x.u32);
    X(x.u33);
    X(x.u34);
    X(x.u35);
    X(x.u36);
    X(x.u37);
    X(x.u38);
    X(x.u39);
    X(x.u40);
    X(x.u41);
    X(x.u42);
    X(x.u43);
    X(x.u44);
    X(x.u45);
    X(x.u46);
    X(x.u47);
    X(x.u48);
    X(x.u49);
    X(x.u50);
    X(x.u51);
    X(x.u52);
    X(x.u53);
    X(x.u54);
    X(x.u55);
    X(x.u56);
    X(x.u57);
    X(x.u58);
    X(x.u59);
    X(x.u60);
    X(x.u61);
    X(x.u62);
    X(x.u63);
    X(x.u64);

    return 0;
}

Oto wynik działania programu skompilowany z 64-bitowym clangiem:

typeof(x.s1): long long int
typeof(x.s2): long long int
typeof(x.s3): long long int
typeof(x.s4): long long int
typeof(x.s5): long long int
typeof(x.s6): long long int
typeof(x.s7): long long int
typeof(x.s8): long long int
typeof(x.s9): long long int
typeof(x.s10): long long int
typeof(x.s11): long long int
typeof(x.s12): long long int
typeof(x.s13): long long int
typeof(x.s14): long long int
typeof(x.s15): long long int
typeof(x.s16): long long int
typeof(x.s17): long long int
typeof(x.s18): long long int
typeof(x.s19): long long int
typeof(x.s20): long long int
typeof(x.s21): long long int
typeof(x.s22): long long int
typeof(x.s23): long long int
typeof(x.s24): long long int
typeof(x.s25): long long int
typeof(x.s26): long long int
typeof(x.s27): long long int
typeof(x.s28): long long int
typeof(x.s29): long long int
typeof(x.s30): long long int
typeof(x.s31): long long int
typeof(x.s32): long long int
typeof(x.s33): long long int
typeof(x.s34): long long int
typeof(x.s35): long long int
typeof(x.s36): long long int
typeof(x.s37): long long int
typeof(x.s38): long long int
typeof(x.s39): long long int
typeof(x.s40): long long int
typeof(x.s41): long long int
typeof(x.s42): long long int
typeof(x.s43): long long int
typeof(x.s44): long long int
typeof(x.s45): long long int
typeof(x.s46): long long int
typeof(x.s47): long long int
typeof(x.s48): long long int
typeof(x.s49): long long int
typeof(x.s50): long long int
typeof(x.s51): long long int
typeof(x.s52): long long int
typeof(x.s53): long long int
typeof(x.s54): long long int
typeof(x.s55): long long int
typeof(x.s56): long long int
typeof(x.s57): long long int
typeof(x.s58): long long int
typeof(x.s59): long long int
typeof(x.s60): long long int
typeof(x.s61): long long int
typeof(x.s62): long long int
typeof(x.s63): long long int
typeof(x.s64): long long int
typeof(x.u1): unsigned long long int
typeof(x.u2): unsigned long long int
typeof(x.u3): unsigned long long int
typeof(x.u4): unsigned long long int
typeof(x.u5): unsigned long long int
typeof(x.u6): unsigned long long int
typeof(x.u7): unsigned long long int
typeof(x.u8): unsigned long long int
typeof(x.u9): unsigned long long int
typeof(x.u10): unsigned long long int
typeof(x.u11): unsigned long long int
typeof(x.u12): unsigned long long int
typeof(x.u13): unsigned long long int
typeof(x.u14): unsigned long long int
typeof(x.u15): unsigned long long int
typeof(x.u16): unsigned long long int
typeof(x.u17): unsigned long long int
typeof(x.u18): unsigned long long int
typeof(x.u19): unsigned long long int
typeof(x.u20): unsigned long long int
typeof(x.u21): unsigned long long int
typeof(x.u22): unsigned long long int
typeof(x.u23): unsigned long long int
typeof(x.u24): unsigned long long int
typeof(x.u25): unsigned long long int
typeof(x.u26): unsigned long long int
typeof(x.u27): unsigned long long int
typeof(x.u28): unsigned long long int
typeof(x.u29): unsigned long long int
typeof(x.u30): unsigned long long int
typeof(x.u31): unsigned long long int
typeof(x.u32): unsigned long long int
typeof(x.u33): unsigned long long int
typeof(x.u34): unsigned long long int
typeof(x.u35): unsigned long long int
typeof(x.u36): unsigned long long int
typeof(x.u37): unsigned long long int
typeof(x.u38): unsigned long long int
typeof(x.u39): unsigned long long int
typeof(x.u40): unsigned long long int
typeof(x.u41): unsigned long long int
typeof(x.u42): unsigned long long int
typeof(x.u43): unsigned long long int
typeof(x.u44): unsigned long long int
typeof(x.u45): unsigned long long int
typeof(x.u45): unsigned long long int
typeof(x.u46): unsigned long long int
typeof(x.u47): unsigned long long int
typeof(x.u48): unsigned long long int
typeof(x.u49): unsigned long long int
typeof(x.u50): unsigned long long int
typeof(x.u51): unsigned long long int
typeof(x.u52): unsigned long long int
typeof(x.u53): unsigned long long int
typeof(x.u54): unsigned long long int
typeof(x.u55): unsigned long long int
typeof(x.u56): unsigned long long int
typeof(x.u57): unsigned long long int
typeof(x.u58): unsigned long long int
typeof(x.u59): unsigned long long int
typeof(x.u60): unsigned long long int
typeof(x.u61): unsigned long long int
typeof(x.u62): unsigned long long int
typeof(x.u63): unsigned long long int
typeof(x.u64): unsigned long long int

Wszystkie pola bitowe wydają się mieć określony typ, a nie typ specyficzny dla określonej szerokości.

Oto wyjście programu skompilowane z 64-bitowym gcc:

typestr(x.s1): other
typestr(x.s2): other
typestr(x.s3): other
typestr(x.s4): other
typestr(x.s5): other
typestr(x.s6): other
typestr(x.s7): other
typestr(x.s8): signed char
typestr(x.s9): other
typestr(x.s10): other
typestr(x.s11): other
typestr(x.s12): other
typestr(x.s13): other
typestr(x.s14): other
typestr(x.s15): other
typestr(x.s16): short
typestr(x.s17): other
typestr(x.s18): other
typestr(x.s19): other
typestr(x.s20): other
typestr(x.s21): other
typestr(x.s22): other
typestr(x.s23): other
typestr(x.s24): other
typestr(x.s25): other
typestr(x.s26): other
typestr(x.s27): other
typestr(x.s28): other
typestr(x.s29): other
typestr(x.s30): other
typestr(x.s31): other
typestr(x.s32): int
typestr(x.s33): other
typestr(x.s34): other
typestr(x.s35): other
typestr(x.s36): other
typestr(x.s37): other
typestr(x.s38): other
typestr(x.s39): other
typestr(x.s40): other
typestr(x.s41): other
typestr(x.s42): other
typestr(x.s43): other
typestr(x.s44): other
typestr(x.s45): other
typestr(x.s46): other
typestr(x.s47): other
typestr(x.s48): other
typestr(x.s49): other
typestr(x.s50): other
typestr(x.s51): other
typestr(x.s52): other
typestr(x.s53): other
typestr(x.s54): other
typestr(x.s55): other
typestr(x.s56): other
typestr(x.s57): other
typestr(x.s58): other
typestr(x.s59): other
typestr(x.s60): other
typestr(x.s61): other
typestr(x.s62): other
typestr(x.s63): other
typestr(x.s64): long long int
typestr(x.u1): other
typestr(x.u2): other
typestr(x.u3): other
typestr(x.u4): other
typestr(x.u5): other
typestr(x.u6): other
typestr(x.u7): other
typestr(x.u8): unsigned char
typestr(x.u9): other
typestr(x.u10): other
typestr(x.u11): other
typestr(x.u12): other
typestr(x.u13): other
typestr(x.u14): other
typestr(x.u15): other
typestr(x.u16): unsigned short
typestr(x.u17): other
typestr(x.u18): other
typestr(x.u19): other
typestr(x.u20): other
typestr(x.u21): other
typestr(x.u22): other
typestr(x.u23): other
typestr(x.u24): other
typestr(x.u25): other
typestr(x.u26): other
typestr(x.u27): other
typestr(x.u28): other
typestr(x.u29): other
typestr(x.u30): other
typestr(x.u31): other
typestr(x.u32): unsigned int
typestr(x.u33): other
typestr(x.u34): other
typestr(x.u35): other
typestr(x.u36): other
typestr(x.u37): other
typestr(x.u38): other
typestr(x.u39): other
typestr(x.u40): other
typestr(x.u41): other
typestr(x.u42): other
typestr(x.u43): other
typestr(x.u44): other
typestr(x.u45): other
typestr(x.u46): other
typestr(x.u47): other
typestr(x.u48): other
typestr(x.u49): other
typestr(x.u50): other
typestr(x.u51): other
typestr(x.u52): other
typestr(x.u53): other
typestr(x.u54): other
typestr(x.u55): other
typestr(x.u56): other
typestr(x.u57): other
typestr(x.u58): other
typestr(x.u59): other
typestr(x.u60): other
typestr(x.u61): other
typestr(x.u62): other
typestr(x.u63): other
typestr(x.u64): unsigned long long int

Co jest zgodne z każdą szerokością o innym typie.

Wyrażenie E1 << E2ma typ promowanego lewego operandu, więc dowolna szerokość mniejsza niż INT_WIDTHjest promowana intpoprzez promocję liczb całkowitych i dowolna szerokość większa niż INT_WIDTHpozostawiona sama. Wynik wyrażenia powinien być rzeczywiście obcięty do szerokości pola bitowego, jeśli ta szerokość jest większa niżINT_WIDTH . Mówiąc dokładniej, powinien zostać obcięty dla typu niepodpisanego i może być implementacją zdefiniowaną dla typów podpisanych.

To samo powinno wystąpić w przypadku E1 + E2innych operatorów arytmetycznych, jeśli E1lub E2są polami bitowymi o szerokości większej niż int. Argument o mniejszej szerokości jest konwertowany na typ o większej szerokości, a wynik ma również typ typu. To bardzo sprzeczne z intuicją zachowanie, które powoduje wiele nieoczekiwanych wyników, może być przyczyną powszechnego przekonania, że ​​pola bitowe są fałszywe i należy ich unikać.

Wydaje się, że wiele kompilatorów nie zgadza się z tą interpretacją standardu C, a interpretacja ta nie jest oczywista z obecnego brzmienia. Przydatne byłoby wyjaśnienie semantyki operacji arytmetycznych dotyczących operandów pola bitowego w przyszłej wersji standardu C.

chqrlie
źródło
1
Myślę, że kluczowym terminem są „promocje liczb całkowitych”. Omówienie pól bitowych z promocjami liczb całkowitych (C11 §6.3.1.1 - Jeśli intmoże reprezentować wszystkie wartości oryginalnego typu (ograniczone przez szerokość, dla pola bitowego), wartość jest konwertowana na int; w przeciwnym razie jest konwertowany na unsigned intA. Są to tak zwane promocje na liczby całkowite - §6.3.1.8 , §6.7.2.1 ), nie obejmują przypadku, w którym szerokość pola bitowego jest większa niż int.
Jonathan Leffler
1
To nie pomaga, że standardowe liście niezdefiniowany (w najlepszym wypadku realizacja zdefiniowane), jakiego rodzaju są dopuszczone do bitowych pól innych niż int, unsigned inti _Bool.
Jonathan Leffler
1
„dowolna szerokość mniejsza niż 32”, „dowolna szerokość większa niż 32” i „jeśli ta szerokość jest większa niż 32” powinna przypuszczalnie odzwierciedlać liczbę bitów zwykłych, inta nie być stałą 32.
Ben Voigt
1
Zgadzam się, że istnieje problem (niedopatrzenie) w standardzie C. Można by argumentować, że skoro standard nie sankcjonuje użycia uint64_tpól bitowych, standard nie musi o nich nic mówić - powinien być objęty dokumentacją implementacji określonych przez implementację części zachowania pól bitowych. W szczególności dlatego, że 52-bitowe pole bitowe nie mieści się w (32-bitowym), intnie powinno to oznaczać, że są one zamienione na 32-bitowe unsigned int, ale to dosłowny odczyt 6,3. 1.1 mówi.
Jonathan Leffler
1
Ponadto, jeśli C ++ wyraźnie rozwiązało problemy związane z „dużym polem bitowym”, C powinien podążać za tym tropem tak dokładnie, jak to możliwe - chyba że jest coś specyficznego dla C ++ w tej rozdzielczości (co jest mało prawdopodobne).
Jonathan Leffler
2

Problem wydaje się być specyficzny dla 32-bitowego generatora kodu gcc w trybie C:

Możesz porównać kod zestawu za pomocą Godbolt's Compiler Explorer

Oto kod źródłowy tego testu:

#include <stdint.h>

typedef union control {
    uint64_t q;
    struct {
        uint64_t a: 1;
        uint64_t b: 1;
        uint64_t c: 1;
        uint64_t d: 1;
        uint64_t e: 1;
        uint64_t f: 1;
        uint64_t g: 4;
        uint64_t h: 1;
        uint64_t i: 1;
        uint64_t p52: 52;
    } b;
} control_t;

uint64_t test(control_t ctl) {
    return ctl.b.p52 << 12;
}

Wyjście w trybie C (flagi -xc -O2 -m32)

test:
        push    esi
        push    ebx
        mov     ebx, DWORD PTR [esp+16]
        mov     ecx, DWORD PTR [esp+12]
        mov     esi, ebx
        shr     ebx, 12
        shr     ecx, 12
        sal     esi, 20
        mov     edx, ebx
        pop     ebx
        or      esi, ecx
        mov     eax, esi
        shld    edx, esi, 12
        pop     esi
        sal     eax, 12
        and     edx, 1048575
        ret

Problemem jest ostatnia instrukcja, and edx, 1048575która przycina 12 najbardziej znaczących bitów.

Dane wyjściowe w trybie C ++ są identyczne, z wyjątkiem ostatniej instrukcji:

test(control):
        push    esi
        push    ebx
        mov     ebx, DWORD PTR [esp+16]
        mov     ecx, DWORD PTR [esp+12]
        mov     esi, ebx
        shr     ebx, 12
        shr     ecx, 12
        sal     esi, 20
        mov     edx, ebx
        pop     ebx
        or      esi, ecx
        mov     eax, esi
        shld    edx, esi, 12
        pop     esi
        sal     eax, 12
        ret

Dane wyjściowe w trybie 64-bitowym są znacznie prostsze i poprawne, ale różnią się w przypadku kompilatorów C i C ++:

#C code:
test:
        movabs  rax, 4503599627366400
        and     rax, rdi
        ret

# C++ code:
test(control):
        mov     rax, rdi
        and     rax, -4096
        ret

Powinieneś złożyć raport o błędzie na trackerze błędów gcc.

chqrlie
źródło
Moje eksperymenty dotyczyły tylko 64-bitowych celów, ale twój 32-bitowy przypadek jest jeszcze bardziej bisarre. Myślę, że należy zgłosić błąd. Najpierw muszę to sprawdzić w najnowszej dostępnej dla mnie wersji GCC.
Grigorij Rechistov
1
@GrigoryRechistov Biorąc pod uwagę sformułowanie w standardzie C , błędem może być 64-bitowy cel, który nie obciął wyniku do 52 bitów. Osobiście postrzegałbym to w ten sposób.
Andrew Henle