Inkonsistente Kürzung von vorzeichenlosen Bitfeld-Ganzzahlausdrücken zwischen C ++ und C in verschiedenen Compilern

10

Bearbeiten 2 :

Ich habe einen seltsamen Testfehler behoben, als eine Funktion, die sich zuvor in einer C ++ - Quelldatei befand, aber wörtlich in eine C-Datei verschoben wurde, falsche Ergebnisse zurückgab. Die folgende MVE ermöglicht es, das Problem mit GCC zu reproduzieren. Als ich jedoch aus einer Laune heraus das Beispiel mit Clang (und später mit VS) zusammenstellte, erhielt ich ein anderes Ergebnis! Ich kann nicht herausfinden, ob dies als Fehler in einem der Compiler oder als Manifestation eines undefinierten Ergebnisses behandelt werden soll, das vom C- oder C ++ - Standard zugelassen wird. Seltsamerweise warnte mich keiner der Compiler vor dem Ausdruck.

Der Schuldige ist dieser Ausdruck:

ctl.b.p52 << 12;

Hier p52wird eingegeben als uint64_t; Es ist auch Teil einer Gewerkschaft (siehe control_tunten). Die Verschiebungsoperation verliert keine Daten, da das Ergebnis immer noch in 64 Bit passt. Dann beschließt GCC jedoch, das Ergebnis auf 52 Bit zu kürzen, wenn ich den C-Compiler verwende ! Mit dem C ++ - Compiler bleiben alle 64 Ergebnisbits erhalten.

Um dies zu veranschaulichen, kompiliert das folgende Beispielprogramm zwei Funktionen mit identischen Körpern und vergleicht dann ihre Ergebnisse. c_behavior()wird in eine C-Quelldatei und cpp_behavior()in eine C ++ - Datei eingefügt und main()führt den Vergleich durch.

Repository mit dem Beispielcode: https://github.com/grigory-rechistov/c-cpp-bitfields

Der Header common.h definiert eine Vereinigung von 64 Bit breiten Bitfeldern und Ganzzahlen und deklariert zwei Funktionen:

#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

Die Funktionen haben identische Körper, außer dass einer als C und einer als C ++ behandelt wird.

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

Haupt 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 zeigt den Unterschied zwischen den Ergebnissen, die sie zurückgeben:

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

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

Mit Clang verhalten sich C und C ++ jedoch identisch und wie erwartet:

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

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

Mit Visual Studio erhalte ich das gleiche Ergebnis wie mit 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++

Ich habe die Beispiele unter Windows ausprobiert, obwohl das ursprüngliche Problem mit GCC unter Linux entdeckt wurde.

Grigory Rechistov
quelle
1
Bitfelder sind für große Breiten notorisch falsch. Bei dieser Frage bin
chqrlie
@chqrlie Ich habe den C- <<Operator so gelesen, dass er die Kürzung erfordert .
Andrew Henle
Bitte posten Sie ein stackoverflow.com/help/minimal-reproducible-example . Der aktuelle Code hat kein main.cund verursacht wahrscheinlich undefiniertes Verhalten auf verschiedene Weise. IMO wäre es klarer, eine MRE mit einer einzelnen Datei zu veröffentlichen, die beim Kompilieren mit jedem Compiler unterschiedliche Ausgaben erzeugt. Weil C-C ++ Interop vom Standard nicht gut spezifiziert ist. Beachten Sie auch, dass Union Aliasing UB in C ++ verursacht.
MM
@ MM Richtig, es ist verrutscht, als ich die Frage gestellt habe. Ich habe es jetzt hinzugefügt, und ich denke auch, dass es auch eine Idee sein könnte, ein kleines Repository damit zu haben
Grigory Rechistov
@MM "IMO wäre es klarer, eine MRE mit einer einzelnen Datei zu veröffentlichen, die beim Kompilieren mit jedem Compiler unterschiedliche Ausgaben erzeugt." Ich habe nicht darüber nachgedacht, als ich meinen Produktionscode in etwas Kleineres umwandelte, aber es sollte möglich sein Formulieren Sie den Reproducer in eine einzige Datei um.
Grigory Rechistov

Antworten:

6

C und C ++ behandeln die Arten von Bitfeldelementen unterschiedlich.

C 2018 6.7.2.1 10 sagt:

Ein Bitfeld wird so interpretiert, dass es einen vorzeichenbehafteten oder vorzeichenlosen Ganzzahltyp hat, der aus der angegebenen Anzahl von Bits besteht.

Beachten Sie, dass dies nicht spezifisch für den Typ ist - es ist ein ganzzahliger Typ - und es heißt nicht, dass der Typ der Typ ist, der zum Deklarieren des Bitfelds verwendet wurde, wie uint64_t a : 1;in der Frage gezeigt. Dies lässt es offenbar für die Implementierung offen, den Typ auszuwählen.

C ++ 2017 Entwurf n4659 12.2.4 [class.bit] 1 sagt über eine Bitfelddeklaration:

… Das Bitfeldattribut ist nicht Teil des Typs des Klassenmitglieds…

Dies impliziert, dass in einer Deklaration wie uint64_t a : 1;der : 1nicht Teil des Typs des Klassenmitglieds ist a, der Typ also so ist, als ob er wäre uint64_t a;, und somit der Typ von aist uint64_t.

Es scheint also, dass GCC ein Bitfeld in C als einen ganzzahligen Typ von 32 Bit oder schmaler behandelt, wenn es passt, und ein Bitfeld in C ++ als deklarierten Typ, und dies scheint nicht gegen die Standards zu verstoßen.

Eric Postpischil
quelle
Ich habe die Kürzung in C als obligatorisch gemäß 6.5.7 4 gelesen (C18-Wortlaut ist ähnlich): "Das Ergebnis von E1 << E2 sind E1 linksverschobene E2-Bitpositionen; frei gewordene Bits werden mit Nullen gefüllt. Wenn E1 einen vorzeichenlosen Typ hat ist der Wert des Ergebnisses E1 x 2E2, modulo eins mehr als der im Ergebnistyp darstellbare Maximalwert. " E1In diesem Fall handelt es sich um ein 52-Bit-Bitfeld.
Andrew Henle
@ AndrewHenle: Ich verstehe, was du sagst. Der Typ eines n- Bit-Bitfelds ist " n- Bit-Ganzzahl" (wobei die Vorzeichen vorerst vernachlässigt werden). Ich habe es so interpretiert, dass der Typ eines n- Bit-Bitfelds ein ganzzahliger Typ ist, den die Implementierung auswählt. Ich stütze mich ausschließlich auf den Wortlaut in 6.7.2.1 10 und bevorzuge Ihre Interpretation. Aber ein Problem mit dieser ist , dass bei einer uint64_t a : 33eingestellten bis 2 ^ 33-1 in einer Struktur s, dann in einer C - Implementierung mit 32-Bit int, s.a+s.asollte 2 ^ 33-2 aufgrund Umhüllung ergeben, aber Clang produziert 2 ^ 34- 2; es behandelt es anscheinend als uint64_t.
Eric Postpischil
@AndrewHenle: (Mehr zur Begründung: In würden s.a+s.adie üblichen arithmetischen Konvertierungen den Typ von nicht ändern s.a, da er breiter als ist unsigned int, sodass die Arithmetik im 33-Bit-Typ ausgeführt wird.)
Eric Postpischil
aber Clang produziert 2 ^ 34−2; es behandelt es anscheinend als uint64_t. Wenn dies eine 64-Bit-Kompilierung ist, scheint Clang damit konsistent zu sein, wie GCC 64-Bit-Kompilierungen behandelt, indem es nicht abgeschnitten wird. Behandelt Clang 32- und 64-Bit-Kompilierungen unterschiedlich? (Und anscheinend habe ich gerade einen weiteren Grund gelernt, Bitfelder zu vermeiden ...)
Andrew Henle
@AndrewHenle: Nun, der alte Apple Clang 1.7 produziert 2 ^ 32−2 (nicht 2 ^ 33−2; es hat ein bisschen verloren!) Sowohl mit -m32als -m64auch mit der Warnung, dass der Typ eine GCC-Erweiterung ist. Mit Apple Clang 11.0 habe ich keine Bibliotheken, um 32-Bit-Code auszuführen, aber die generierte Assembly zeigt pushl $3und pushl $-2vor dem Aufruf printf, also denke ich, dass das 2 ^ 34−2 ist. Apple Clang unterscheidet sich also nicht zwischen 32-Bit- und 64-Bit-Zielen, sondern hat sich im Laufe der Zeit geändert.
Eric Postpischil
4

Andrew Henle schlug eine strikte Interpretation des C-Standards vor: Der Typ eines Bitfelds ist ein vorzeichenbehafteter oder vorzeichenloser ganzzahliger Typ mit genau der angegebenen Breite.

Hier ist ein Test, der diese Interpretation unterstützt: Mit der C1x- _Generic()Konstruktion versuche ich, den Typ der Bitfelder unterschiedlicher Breite zu bestimmen. Ich musste sie mit dem Typ definieren long long int, um Warnungen beim Kompilieren mit clang zu vermeiden.

Hier ist die Quelle:

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

Hier ist die Ausgabe des Programms, die mit 64-Bit-Clang kompiliert wurde:

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

Alle Bitfelder scheinen eher den definierten Typ als einen für die definierte Breite spezifischen Typ zu haben.

Hier ist die Ausgabe des Programms, die mit 64-Bit-gcc kompiliert wurde:

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

Das stimmt damit überein, dass jede Breite einen anderen Typ hat.

Der Ausdruck E1 << E2hat den Typ des heraufgestuften linken Operanden, also jede Breite, die kleiner ist als INT_WIDTHdie Heraufstufung intüber eine ganzzahlige Heraufstufung, und jede Breite größer als INT_WIDTHallein gelassen. Das Ergebnis des Ausdrucks sollte tatsächlich auf die Breite des Bitfeldes abgeschnitten werden, wenn diese Breite größer als ist INT_WIDTH. Genauer gesagt sollte es für einen vorzeichenlosen Typ abgeschnitten werden, und es kann eine Implementierung sein, die für vorzeichenbehaftete Typen definiert ist.

Dasselbe sollte für E1 + E2und andere arithmetische Operatoren auftreten, wenn E1oder E2Bitfelder mit einer Breite größer als die von sind int. Der Operand mit der kleineren Breite wird in den Typ mit der größeren Breite konvertiert, und das Ergebnis hat auch den Typ type. Dieses sehr kontraintuitive Verhalten, das viele unerwartete Ergebnisse verursacht, kann die weit verbreitete Überzeugung sein, dass Bitfelder falsch sind und vermieden werden sollten.

Viele Compiler scheinen dieser Interpretation des C-Standards weder zu folgen, noch ist diese Interpretation aus dem aktuellen Wortlaut ersichtlich. Es wäre nützlich, die Semantik von arithmetischen Operationen mit Bitfeldoperanden in einer zukünftigen Version des C-Standards zu klären.

chqrlie
quelle
1
Ich denke, der Schlüsselbegriff ist "Integer Promotions". Die Diskussion von Bitfeldern mit ganzzahligen Heraufstufungen (C11 §6.3.1.1 - Wenn ein intalle Werte des ursprünglichen Typs darstellen kann (wie durch die Breite für ein Bitfeld eingeschränkt), wird der Wert in ein intanderes konvertiert wird in ein konvertiert unsigned int. Diese werden als ganzzahlige Heraufstufungen bezeichnet . - §6.3.1.8 , §6.7.2.1 ) decken nicht den Fall ab, in dem die Breite eines Bitfelds breiter als ein ist int.
Jonathan Leffler
1
Es hilft nicht , dass die Standard - Blätter nicht definiert (am besten Implementierung definiert) , welche Arten für Bit-Felder sind erlaubt außer int, unsigned intund _Bool.
Jonathan Leffler
1
"jede Breite kleiner als 32", "jede Breite größer als 32" und "wenn diese Breite größer als 32 ist" sollten vermutlich die Anzahl der Bits in der Ebene widerspiegeln intund keine festen 32 sein.
Ben Voigt
1
Ich bin damit einverstanden, dass der C-Standard ein Problem (der Kontrolle) aufweist. Es könnte Raum für Argumente geben, dass der Standard, da er die Verwendung von uint64_tBitfeldern nicht genehmigt, nichts darüber zu sagen hat - er sollte durch die Dokumentation der Implementierung der implementierungsdefinierten Teile des Verhaltens abgedeckt werden von Bitfeldern. Insbesondere, nur weil die 52-Bit des intBitfelds nicht in ein (32-Bit) passen , sollte dies nicht bedeuten, dass sie in ein 32-Bit zerlegt werden unsigned int, aber das ist eine wörtliche Lesart von 6,3. 1.1 sagt.
Jonathan Leffler
1
Wenn C ++ Probleme mit "großen Bitfeldern" explizit gelöst hat, sollte C diesem Beispiel so genau wie möglich folgen - es sei denn, diese Auflösung enthält etwas Spezifisches für C ++ (was nicht wahrscheinlich ist).
Jonathan Leffler
2

Das Problem scheint spezifisch für den 32-Bit-Codegenerator von gcc im C-Modus zu sein:

Sie können den Assemblycode mit dem Compiler-Explorer von Godbolt vergleichen

Hier ist der Quellcode für diesen 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;
}

Die Ausgabe im C-Modus (Flags -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

Das Problem ist der letzte Befehl and edx, 1048575, der die 12 höchstwertigen Bits abschneidet.

Die Ausgabe im C ++ - Modus ist bis auf die letzte Anweisung identisch:

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

Die Ausgabe im 64-Bit-Modus ist viel einfacher und korrekter, unterscheidet sich jedoch für die C- und C ++ - Compiler:

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

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

Sie sollten einen Fehlerbericht auf dem gcc Bug Tracker einreichen.

chqrlie
quelle
Meine Experimente waren nur für 64-Bit-Ziele, aber Ihr 32-Bit-Fall ist noch bissiger. Ich denke, ein Fehlerbericht ist fällig. Zuerst muss ich eine aktuelle GCC-Version überprüfen, die mir zur Verfügung steht.
Grigory Rechistov
1
@GrigoryRechistov die Formulierung in Anbetracht des C - Standard , kann der Fehler sehr gut das 64-Bit - Ziel sein , andernfalls das Ergebnis zu kürzen bis zu 52 Bit. Ich persönlich würde das so sehen.
Andrew Henle