Troncamento incoerente di espressioni intere di bitfield senza segno tra C ++ e C in diversi compilatori


10

Modifica 2 :

Stavo eseguendo il debug di uno strano errore di test quando una funzione precedentemente residente in un file sorgente C ++ ma spostata in un file C alla lettera, iniziava a restituire risultati errati. Il MVE qui sotto consente di riprodurre il problema con GCC. Tuttavia, quando, per un capriccio, ho compilato l'esempio con Clang (e più tardi con VS), ho ottenuto un risultato diverso! Non riesco a capire se trattare questo come un bug in uno dei compilatori o come manifestazione di risultati indefiniti consentiti dallo standard C o C ++. Stranamente, nessuno dei compilatori mi ha dato alcun avvertimento sull'espressione.

Il colpevole è questa espressione:

ctl.b.p52 << 12;

Qui, p52viene digitato come uint64_t; fa anche parte di un'unione (vedi control_tsotto). L'operazione di spostamento non perde alcun dato poiché il risultato si adatta ancora a 64 bit. Tuttavia, GCC decide di troncare il risultato a 52 bit se uso il compilatore C ! Con il compilatore C ++, vengono conservati tutti i 64 bit del risultato.

Per illustrare ciò, il seguente programma di esempio compila due funzioni con corpi identici, quindi confronta i loro risultati. c_behavior()viene inserito in un file sorgente C e cpp_behavior()in un file C ++ e main()fa il confronto.

Repository con il codice di esempio: https://github.com/grigory-rechistov/c-cpp-bitfields

L'intestazione common.h definisce un'unione di campi di bit e intero a 64 bit e dichiara due funzioni:

#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

Le funzioni hanno corpi identici, tranne per il fatto che uno è trattato come C e un altro come 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 mostra la differenza tra i risultati che restituiscono:

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

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

Tuttavia, con Clang C e C ++ si comportano in modo identico e come previsto:

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

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

Con Visual Studio ottengo lo stesso risultato di Clang:

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++

Ho provato gli esempi su Windows, anche se il problema originale con GCC è stato scoperto su Linux.


1
i bit-field sono notoriamente fasulli per grandi larghezze. Mi sono imbattuto in problemi simili in questa domanda: stackoverflow.com/questions/58846584/...
chqrlie

@chqrlie Ho letto l' operatore C<< che richiede il troncamento.
Andrew Henle,

Invia un stackoverflow.com/help/minimal-reproducible-example . Il codice corrente non ha main.ce probabilmente causa comportamenti indefiniti in diversi modi. IMO sarebbe più chiaro pubblicare un MRE a file singolo che produce output diversi quando compilato con ciascun compilatore. Perché l'interoperabilità C-C ++ non è ben specificata dallo standard. Si noti inoltre che l'aliasing unione provoca UB in C ++.
MM

@MM Giusto, è scivolato quando stavo postando la domanda. L'ho aggiunto ora, e penso anche che avere un piccolo repository con esso potrebbe anche essere un'idea
Grigory Rechistov il

@MM "IMO sarebbe più chiaro pubblicare un MRE a file singolo che produce output diversi quando compilato con ciascun compilatore." Non ci ho pensato mentre stavo trasformando il mio codice di produzione in qualcosa di più piccolo, ma dovrebbe essere possibile riformulare il riproduttore in un singolo file.
Grigory Rechistov il

Risposte:


6

C e C ++ trattano i tipi di membri bit-field in modo diverso.

C 2018 6.7.2.1 10 dice:

Un campo di bit viene interpretato come un tipo intero con segno o senza segno costituito dal numero specificato di bit ...

Si noti che questo non è specifico per il tipo - è un tipo intero - e non dice che il tipo è il tipo che è stato usato per dichiarare il campo di bit, come uint64_t a : 1;mostrato nella domanda. Ciò a quanto pare lascia aperto all'implementazione la scelta del tipo.

C ++ 2017 draft n4659 12.2.4 [class.bit] 1 dice, di una dichiarazione in bit:

... L'attributo bit-field non fa parte del tipo di membro della classe ...

Ciò implica che, in una dichiarazione come uint64_t a : 1;, il : 1non fa parte del tipo del membro della classe a, quindi il tipo è come se fosse uint64_t a;, e quindi il tipo di aè uint64_t.

Quindi sembra che GCC tratti un campo bit in C come un tipo intero a 32 bit o più stretto se si adatta e un campo bit in C ++ come il suo tipo dichiarato, e questo non sembra violare gli standard.


Ho letto il troncamento in C come obbligatorio per 6.5.7 4 (la dicitura C18 è simile): "Il risultato di E1 << E2 è E1 posizioni bit E2 spostate a sinistra; i bit liberati sono riempiti con zeri. Se E1 ha un tipo senza segno , il valore del risultato è E1 x 2E2, modulo ridotto di uno in più rispetto al valore massimo rappresentabile nel tipo di risultato. " E1in questo caso è un campo bit a 52 bit.
Andrew Henle,

@AndrewHenle: capisco cosa stai dicendo. Il tipo di un campo bit n -bit è " n -bit integer" (trascurando la firma per ora). Lo stavo interpretando come il tipo di un bit-field n -bit è un tipo intero, che l'implementazione sceglie. Basandomi esclusivamente sulla formulazione del 6.7.2.1 10, favorisco la tua interpretazione. Ma un problema è che, dato un uint64_t a : 33set a 2 ^ 33−1 in una struttura s, quindi, in un'implementazione C con 32 bit int, s.a+s.adovrebbe produrre 2 ^ 33−2 a causa del wrapping, ma Clang produce 2 ^ 34− 2; a quanto pare lo tratta come uint64_t.
Eric Postpischil,

@AndrewHenle: (Altre informazioni sul ragionamento: in s.a+s.a, le solite conversioni aritmetiche non cambierebbero il tipo di s.a, poiché è più ampio di unsigned int, quindi l'aritmetica verrebbe eseguita nel tipo a 33 bit.)
Eric Postpischil,

ma Clang produce 2 ^ 34−2; a quanto pare lo tratta come uint64_t. Se si tratta di una compilazione a 64 bit, sembra che Clang sia coerente con il modo in cui GCC sta trattando le compilazioni a 64 bit non troncandole. Clang tratta le compilazioni a 32 e 64 bit in modo diverso? (E sembra che ho appena imparato un altro motivo per evitare i piccoli campi ...)
Andrew Henle il

@AndrewHenle: Beh, la vecchia Apple Clang 1.7 produce 2 ^ 32−2 (non 2 ^ 33−2; ha perso un po '!) Sia con -m32che -m64, con un avvertimento che il tipo è un'estensione GCC. Con Apple Clang 11.0, non ho librerie per eseguire il codice a 32 bit, ma l'assembly generato mostra pushl $3e pushl $-2prima di chiamare printf, quindi penso che sia 2 ^ 34−2. Quindi Apple Clang non differisce tra target a 32 e 64 bit, ma è cambiata nel tempo.
Eric Postpischil,

4

Andrew Henle ha suggerito un'interpretazione rigorosa dello standard C: il tipo di campo bit è un tipo intero con segno o senza segno con esattamente la larghezza specificata.

Ecco un test che supporta questa interpretazione: usando la _Generic()costruzione C1x , sto cercando di determinare il tipo di campi bit di diverse larghezze. Ho dovuto definirli con il tipo long long intper evitare avvisi durante la compilazione con clang.

Ecco la fonte:

#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;
}

Ecco l'output del programma compilato con clang a 64 bit:

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

Tutti i campi di bit sembrano avere il tipo definito anziché un tipo specifico per la larghezza definita.

Ecco l'output del programma compilato con gcc a 64 bit:

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

Che è coerente con ogni larghezza che ha un tipo diverso.

L'espressione E1 << E2ha il tipo di operando di sinistra promosso, quindi qualsiasi larghezza inferiore a quella che INT_WIDTHviene promossa inttramite la promozione di numeri interi e qualsiasi larghezza maggiore di quella INT_WIDTHlasciata sola. Il risultato dell'espressione dovrebbe infatti essere troncato alla larghezza del campo bit se questa larghezza è maggiore di INT_WIDTH. Più precisamente, dovrebbe essere troncato per un tipo senza segno e potrebbe essere definito per l'implementazione per i tipi con segno.

Lo stesso dovrebbe accadere per E1 + E2e altri operatori aritmetici se E1o E2sono campi di bit con una larghezza maggiore di quella di int. L'operando con la larghezza minore viene convertito nel tipo con la larghezza maggiore e il risultato ha anche il tipo di tipo. Questo comportamento molto controintuitivo che causa molti risultati inaspettati, può essere la causa della diffusa convinzione che i campi di bit siano falsi e dovrebbero essere evitati.

Molti compilatori non sembrano seguire questa interpretazione dello standard C, né questa interpretazione è evidente dalla formulazione attuale. Sarebbe utile chiarire la semantica delle operazioni aritmetiche che coinvolgono operandi bit-field in una versione futura dello standard C.


1
Penso che il termine chiave sia "promozioni intere". La discussione di campi di bit con promozioni di numeri interi (C11 §6.3.1.1 - Se un intpuò rappresentare tutti i valori del tipo originale (come limitato dalla larghezza, per un campo di bit), il valore viene convertito in un int; altrimenti, esso viene convertito in unsigned intA. Queste sono chiamate promozioni intere. - §6.3.1.8 , §6.7.2.1 ), non coprono il caso in cui la larghezza di un campo bit è più ampia di un int.
Jonathan Leffler il

1
Non aiuta che lo standard lasci indefiniti (nella migliore delle ipotesi definiti dall'implementazione) quali tipi sono consentiti per i campi di bit diversi da int, unsigned inte _Bool.
Jonathan Leffler,

1
"Qualsiasi larghezza inferiore a 32", "Qualsiasi larghezza maggiore di 32" e "Se questa larghezza è maggiore di 32" dovrebbe presumibilmente riflettere il numero di bit in chiaro inte non essere un 32 fisso.
Ben Voigt

1
Sono d'accordo che c'è un problema (di supervisione) nello standard C. Potrebbe esserci spazio per sostenere che, poiché lo standard non sancisce l'uso di uint64_tbit-field, lo standard non deve dire nulla al riguardo - dovrebbe essere coperto dalla documentazione dell'implementazione delle parti del comportamento definite dall'implementazione di bit-field. In particolare, solo perché i 52 bit del campo di bit non rientrano in un (32 bit), intciò non significa che siano scricchiolati in un 32 bit unsigned int, ma è una lettura letterale di 6.3. 1.1 dice.
Jonathan Leffler il

1
Inoltre, se C ++ ha risolto in modo esplicito i problemi del "grande campo di bit", allora C dovrebbe seguire quel vantaggio il più vicino possibile - a meno che non ci sia qualcosa di intrinsecamente specifico in C ++ in merito a quella risoluzione (il che non è probabile).
Jonathan Leffler il

2

Il problema sembra essere specifico per il generatore di codice a 32 bit di gcc in modalità C:

È possibile confrontare il codice assembly utilizzando Godbolt's Compiler Explorer

Ecco il codice sorgente per questo test:

#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;
}

L'output in modalità C (flag -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

Il problema è l'ultima istruzione and edx, 1048575che taglia i 12 bit più significativi.

L'output in modalità C ++ è identico ad eccezione dell'ultima istruzione:

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

L'output in modalità 64 bit è molto più semplice e corretto, ma diverso per i compilatori C e C ++:

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

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

Dovresti presentare una segnalazione di bug sul tracker di bug di gcc.


I miei esperimenti erano solo per target a 64 bit, ma il tuo caso a 32 bit è ancora più bizzarro. Immagino che una segnalazione di bug sia dovuta. Innanzitutto, devo ricontrollarlo su una versione più recente di GCC disponibile per me.
Grigory Rechistov il

1
@GrigoryRechistov Dato il testo nello standard C , il bug potrebbe benissimo essere la destinazione a 64 bit che non riesce a troncare il risultato fino a 52 bit. Personalmente lo vedrei in quel modo.
Andrew Henle,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.