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 p52
wird eingegeben als uint64_t
; Es ist auch Teil einer Gewerkschaft (siehe control_t
unten). 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.
quelle
<<
Operator so gelesen, dass er die Kürzung erfordert .main.c
und 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.Antworten:
C und C ++ behandeln die Arten von Bitfeldelementen unterschiedlich.
C 2018 6.7.2.1 10 sagt:
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:
Dies impliziert, dass in einer Deklaration wie
uint64_t a : 1;
der: 1
nicht Teil des Typs des Klassenmitglieds ista
, der Typ also so ist, als ob er wäreuint64_t a;
, und somit der Typ vona
istuint64_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.
quelle
E1
In diesem Fall handelt es sich um ein 52-Bit-Bitfeld.uint64_t a : 33
eingestellten bis 2 ^ 33-1 in einer Strukturs
, dann in einer C - Implementierung mit 32-Bitint
,s.a+s.a
sollte 2 ^ 33-2 aufgrund Umhüllung ergeben, aber Clang produziert 2 ^ 34- 2; es behandelt es anscheinend alsuint64_t
.s.a+s.a
die üblichen arithmetischen Konvertierungen den Typ von nicht änderns.a
, da er breiter als istunsigned int
, sodass die Arithmetik im 33-Bit-Typ ausgeführt wird.)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 ...)-m32
als-m64
auch 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 zeigtpushl $3
undpushl $-2
vor dem Aufrufprintf
, 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.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 definierenlong long int
, um Warnungen beim Kompilieren mit clang zu vermeiden.Hier ist die Quelle:
Hier ist die Ausgabe des Programms, die mit 64-Bit-Clang kompiliert wurde:
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:
Das stimmt damit überein, dass jede Breite einen anderen Typ hat.
Der Ausdruck
E1 << E2
hat den Typ des heraufgestuften linken Operanden, also jede Breite, die kleiner ist alsINT_WIDTH
die Heraufstufungint
über eine ganzzahlige Heraufstufung, und jede Breite größer alsINT_WIDTH
allein gelassen. Das Ergebnis des Ausdrucks sollte tatsächlich auf die Breite des Bitfeldes abgeschnitten werden, wenn diese Breite größer als istINT_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 + E2
und andere arithmetische Operatoren auftreten, wennE1
oderE2
Bitfelder mit einer Breite größer als die von sindint
. 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.
quelle
int
alle Werte des ursprünglichen Typs darstellen kann (wie durch die Breite für ein Bitfeld eingeschränkt), wird der Wert in einint
anderes konvertiert wird in ein konvertiertunsigned 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 istint
.int
,unsigned int
und_Bool
.int
und keine festen 32 sein.uint64_t
Bitfeldern 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 desint
Bitfelds nicht in ein (32-Bit) passen , sollte dies nicht bedeuten, dass sie in ein 32-Bit zerlegt werdenunsigned int
, aber das ist eine wörtliche Lesart von 6,3. 1.1 sagt.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:
Die Ausgabe im C-Modus (Flags
-xc -O2 -m32
)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:
Die Ausgabe im 64-Bit-Modus ist viel einfacher und korrekter, unterscheidet sich jedoch für die C- und C ++ - Compiler:
Sie sollten einen Fehlerbericht auf dem gcc Bug Tracker einreichen.
quelle