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 p52
jest wpisany jako uint64_t
; jest także częścią związku (patrz control_t
poniż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.
źródło
<<
jako wymagającego obcięcia.main.c
i 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 ++.Odpowiedzi:
C i C ++ traktują typy elementów pola bitowego inaczej.
C 2018 6.7.2.1 10 mówi:
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:
Oznacza to, że w deklaracji, takich jak
uint64_t a : 1;
The: 1
nie jest częścią rodzaju członka klasya
, więc typ to tak, jakby to byłouint64_t a;
, a tym samym typema
jestuint64_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.
źródło
E1
w tym przypadku jest to 52-bitowe pole bitowe.uint64_t a : 33
zestaw do 2 ^ 33-1 w strukturzes
, a następnie, w implementacji C z 32-bitowymint
,s.a+s.a
powinien dawać 2 ^ 33-2 powodu opakowania, ale Clang wywołuje 2 ^ 34- 2; najwyraźniej traktuje to jakouint64_t
.s.a+s.a
zwykłe konwersje arytmetyczne nie zmieniłyby typus.a
, ponieważ jest szerszy niżunsigned int
, więc arytmetyka byłaby wykonywana w typie 33-bitowym.)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 ...)-m32
i-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 pokazujepushl $3
ipushl $-2
przed wywołaniemprintf
, 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ł.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 int
aby uniknąć ostrzeżeń podczas kompilacji z clang.Oto źródło:
Oto wynik działania programu skompilowany z 64-bitowym clangiem:
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:
Co jest zgodne z każdą szerokością o innym typie.
Wyrażenie
E1 << E2
ma typ promowanego lewego operandu, więc dowolna szerokość mniejsza niżINT_WIDTH
jest promowanaint
poprzez promocję liczb całkowitych i dowolna szerokość większa niżINT_WIDTH
pozostawiona 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 + E2
innych operatorów arytmetycznych, jeśliE1
lubE2
są 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.
źródło
int
może reprezentować wszystkie wartości oryginalnego typu (ograniczone przez szerokość, dla pola bitowego), wartość jest konwertowana naint
; w przeciwnym razie jest konwertowany naunsigned int
A. 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
.int
,unsigned int
i_Bool
.int
a nie być stałą 32.uint64_t
pó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),int
nie powinno to oznaczać, że są one zamienione na 32-bitoweunsigned int
, ale to dosłowny odczyt 6,3. 1.1 mówi.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:
Wyjście w trybie C (flagi
-xc -O2 -m32
)Problemem jest ostatnia instrukcja,
and edx, 1048575
która przycina 12 najbardziej znaczących bitów.Dane wyjściowe w trybie C ++ są identyczne, z wyjątkiem ostatniej instrukcji:
Dane wyjściowe w trybie 64-bitowym są znacznie prostsze i poprawne, ale różnią się w przypadku kompilatorów C i C ++:
Powinieneś złożyć raport o błędzie na trackerze błędów gcc.
źródło