Edit 2 :
Saya sedang men-debug kegagalan pengujian aneh ketika fungsi yang sebelumnya berada di file sumber C ++ tetapi pindah ke file C kata demi kata, mulai mengembalikan hasil yang salah. MVE di bawah ini memungkinkan untuk mereproduksi masalah dengan GCC. Namun, ketika saya, dengan hati-hati, mengkompilasi contoh dengan Dentang (dan kemudian dengan VS), saya mendapat hasil yang berbeda! Saya tidak tahu apakah akan memperlakukan ini sebagai bug di salah satu kompiler, atau sebagai manifestasi dari hasil yang tidak ditentukan yang diizinkan oleh standar C atau C ++. Anehnya, tidak ada kompiler yang memberi saya peringatan tentang ekspresi.
Pelakunya adalah ungkapan ini:
ctl.b.p52 << 12;
Di sini, p52
diketik sebagai uint64_t
; itu juga merupakan bagian dari persatuan (lihat di control_t
bawah). Operasi shift tidak kehilangan data apa pun karena hasilnya masih sesuai dengan 64 bit. Namun, kemudian GCC memutuskan untuk memotong hasilnya menjadi 52 bit jika saya menggunakan kompiler C ! Dengan kompiler C ++, semua 64 bit hasil dipertahankan.
Untuk menggambarkan ini, contoh program di bawah ini mengkompilasi dua fungsi dengan tubuh yang identik, dan kemudian membandingkan hasilnya. c_behavior()
ditempatkan dalam file sumber C dan cpp_behavior()
dalam file C ++, dan main()
melakukan perbandingan.
Repositori dengan kode contoh: https://github.com/grigory-rechistov/c-cpp-bitfields
Header common.h mendefinisikan gabungan bitfield lebar dan integer 64-bit dan mendeklarasikan dua fungsi:
#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
Fungsi-fungsinya memiliki tubuh yang identik, kecuali bahwa satu diperlakukan sebagai C dan yang lainnya sebagai 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 menunjukkan perbedaan antara hasil yang mereka kembalikan:
$ gcc -Wpedantic main.c c-part.c cpp-part.cpp
$ ./a.exe
OMG C != C++
Namun, dengan Dentang C dan C ++ berperilaku identik dan seperti yang diharapkan:
$ clang -Wpedantic main.c c-part.c cpp-part.cpp
$ ./a.exe
C == C++
Dengan Visual Studio saya mendapatkan hasil yang sama dengan Dentang:
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++
Saya mencoba contoh-contoh di Windows, meskipun masalah asli dengan GCC ditemukan di Linux.
sumber
<<
Operator sebagai membutuhkan pemotongan.main.c
dan mungkin menyebabkan perilaku tidak terdefinisi dalam beberapa cara. IMO akan lebih jelas untuk memposting satu file MRE yang menghasilkan output berbeda ketika dikompilasi dengan masing-masing kompiler. Karena C-C ++ interop tidak ditentukan dengan baik oleh standar. Juga perhatikan bahwa aliasing serikat menyebabkan UB dalam C ++.Jawaban:
C dan C ++ memperlakukan jenis anggota bit-field secara berbeda.
C 2018 6.7.2.1 10 mengatakan:
Perhatikan ini bukan spesifik tentang tipe-itu adalah tipe integer-dan tidak mengatakan tipe adalah tipe yang digunakan untuk mendeklarasikan bit-field, seperti pada
uint64_t a : 1;
ditunjukkan dalam pertanyaan. Ini tampaknya membiarkannya terbuka pada implementasi untuk memilih jenisnya.C ++ 2017 draft n4659 12.2.4 [class.bit] 1 mengatakan, dari deklarasi bit-field:
Ini berarti bahwa, dalam deklarasi seperti
uint64_t a : 1;
, yang: 1
bukan bagian dari jenis anggota kelasa
, sehingga jenis ini seolah-olah ituuint64_t a;
, dan dengan demikian jenisa
iniuint64_t
.Jadi tampaknya GCC memperlakukan bidang-bit dalam C sebagai beberapa tipe integer 32-bit atau lebih sempit jika cocok dan bidang-bit dalam C ++ sebagai tipe yang dideklarasikan, dan ini tampaknya tidak melanggar standar.
sumber
E1
dalam hal ini adalah bidang bit 52-bit.uint64_t a : 33
set ke 2 ^ 33−1 dalam sebuah strukturs
, maka, dalam implementasi C dengan 32-bitint
,s.a+s.a
harus menghasilkan 2 ^ 33−2 karena pembungkus, tetapi Dentang menghasilkan 2 ^ 34− 2; tampaknya memperlakukannya sebagaiuint64_t
.s.a+s.a
, konversi aritmatika yang biasa tidak akan mengubah jeniss.a
, karena lebih luas dariunsigned int
, sehingga aritmatika akan dilakukan dalam tipe 33-bit.)uint64_t
. Jika itu kompilasi 64-bit, itu sepertinya membuat Dentang konsisten dengan bagaimana GCC memperlakukan kompilasi 64-bit dengan tidak memotong. Apakah Dentang memperlakukan kompilasi 32- dan 64-bit berbeda? (Dan sepertinya saya baru saja belajar alasan lain untuk menghindari bidang bit ...)-m32
dan-m64
, dengan peringatan bahwa jenisnya adalah ekstensi GCC. Dengan Apple Clang 11.0, saya tidak memiliki perpustakaan untuk menjalankan kode 32-bit, tetapi perakitan yang dihasilkan menunjukkanpushl $3
danpushl $-2
sebelum memanggilprintf
, jadi saya pikir itu adalah 2 ^ 34−2. Jadi Apple Clang tidak berbeda antara target 32-bit dan 64-bit tetapi memang berubah seiring waktu.Andrew Henle menyarankan interpretasi ketat dari Standar C: tipe bit-field adalah tipe integer yang bertanda tangan atau tidak bertanda tangan dengan lebar persis seperti yang ditentukan.
Berikut ini adalah tes yang mendukung interpretasi ini: menggunakan
_Generic()
konstruksi C1x , saya mencoba menentukan jenis bidang bit dengan lebar yang berbeda. Saya harus mendefinisikan mereka dengan tipelong long int
untuk menghindari peringatan ketika kompilasi dengan dentang.Inilah sumbernya:
Berikut adalah output program yang dikompilasi dengan dentang 64-bit:
Semua bidang bit tampaknya memiliki tipe yang ditentukan dan bukan tipe yang spesifik untuk lebar yang ditentukan.
Ini adalah output program yang dikompilasi dengan 64-bit gcc:
Yang konsisten dengan masing-masing lebar memiliki tipe yang berbeda.
Ekspresi
E1 << E2
memiliki jenis operan kiri yang dipromosikan, sehingga lebar yang kurang dariINT_WIDTH
dipromosikanint
melalui promosi bilangan bulat dan lebar lebih besar dariINT_WIDTH
yang dibiarkan sendiri. Hasil ekspresi memang harus dipotong dengan lebar bidang-bit jika lebar ini lebih besar dariINT_WIDTH
. Lebih tepatnya, itu harus dipotong untuk tipe yang tidak ditandatangani dan mungkin implementasi yang ditentukan untuk tipe yang ditandatangani.Hal yang sama harus terjadi untuk
E1 + E2
dan operator aritmatika lainnya jikaE1
atauE2
adalah bit-bidang dengan lebar lebih besar dariint
. Operand dengan lebar yang lebih kecil dikonversi ke tipe dengan lebar yang lebih besar dan hasilnya memiliki tipe type juga. Perilaku yang sangat kontra-intuitif ini menyebabkan banyak hasil yang tidak terduga, mungkin menjadi penyebab kepercayaan luas bahwa bit-field adalah palsu dan harus dihindari.Banyak kompiler tampaknya tidak mengikuti interpretasi Standar C ini, juga interpretasi ini tidak jelas dari kata-kata saat ini. Akan bermanfaat untuk memperjelas semantik operasi aritmatika yang melibatkan operan bit-field dalam versi C Standard yang akan datang.
sumber
int
dapat mewakili semua nilai dari tipe asli (sebagaimana dibatasi oleh lebar, untuk bidang-bit), nilainya dikonversikan keint
; dikonversi menjadiunsigned int
. Ini disebut promosi bilangan bulat. - §6.3.1.8 , §6.7.2.1 ), jangan tutupi kasus di mana lebar bidang bit lebih lebar dari aint
.int
,unsigned int
dan_Bool
.int
dan tidak menjadi tetap 32.uint64_t
bit-bidang, standar tidak perlu mengatakan apa-apa tentang mereka - itu harus dicakup oleh dokumentasi implementasi dari bagian perilaku yang ditentukan implementasi dari perilaku. bit-bidang. Khususnya, hanya karena 52-bit dari bit-field tidak cocok dengan (32-bit),int
itu tidak berarti bahwa mereka digesek menjadi 32-bitunsigned int
, tapi itulah yang membaca 6,3 secara literal. 1.1 kata.Masalahnya tampaknya khusus untuk generator kode 32-bit gcc dalam mode C:
Anda dapat membandingkan kode perakitan menggunakan Explorer Kompiler Godbolt
Berikut adalah kode sumber untuk tes ini:
Output dalam mode C (flags
-xc -O2 -m32
)Masalahnya adalah instruksi terakhir
and edx, 1048575
yang memotong 12 bit paling signifikan.Output dalam mode C ++ identik kecuali untuk instruksi terakhir:
Output dalam mode 64-bit jauh lebih sederhana dan benar, namun berbeda untuk kompiler C dan C ++:
Anda harus mengajukan laporan bug pada pelacak bug gcc.
sumber