Изменить 2 :
Я отлаживал странный тестовый сбой, когда функция, ранее находившаяся в исходном файле C ++, но дословно перемещенная в файл C, начала возвращать неверные результаты. MVE ниже позволяет воспроизвести проблему с GCC. Однако, когда я по прихоти скомпилировал пример с Clang (а позже с VS), я получил другой результат! Я не могу понять, следует ли рассматривать это как ошибку в одном из компиляторов или как проявление неопределенного результата, разрешенного стандартом C или C ++. Как ни странно, ни один из компиляторов не дал мне никаких предупреждений о выражении.
Виновником является это выражение:
ctl.b.p52 << 12;
Здесь p52
печатается как uint64_t
; это также часть союза (см. control_t
ниже). Операция сдвига не теряет никаких данных, поскольку результат все еще умещается в 64 бита. Однако тогда GCC решает урезать результат до 52 бит, если я использую компилятор C ! С компилятором C ++ все 64 бита результата сохраняются.
Чтобы проиллюстрировать это, пример программы ниже компилирует две функции с одинаковыми телами, а затем сравнивает их результаты. c_behavior()
помещается в исходный файл C и cpp_behavior()
в файл C ++ и main()
выполняет сравнение.
Репозиторий с примером кода: https://github.com/grigory-rechistov/c-cpp-bitfields
Заголовок common.h определяет объединение 64-битных битовых полей и целых чисел и объявляет две функции:
#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
Функции имеют идентичные тела, за исключением того, что одно рассматривается как 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 показывает разницу между результатами, которые они возвращают:
$ gcc -Wpedantic main.c c-part.c cpp-part.cpp
$ ./a.exe
OMG C != C++
Однако с Clang C и C ++ ведут себя одинаково и, как и ожидалось:
$ clang -Wpedantic main.c c-part.c cpp-part.cpp
$ ./a.exe
C == C++
С Visual Studio я получаю тот же результат, что и с 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++
Я попробовал примеры на Windows, хотя первоначальная проблема с GCC была обнаружена на Linux.
источник
<<
как требующий усечения.main.c
и, вероятно, вызывает неопределенное поведение несколькими способами. IMO было бы яснее опубликовать MRE с одним файлом, который выдает разные результаты при компиляции с каждым компилятором. Потому что взаимодействие C-C ++ не определено стандартом. Также обратите внимание, что объединение псевдонимов вызывает UB в C ++.Ответы:
C и C ++ по-разному относятся к типам членов битовых полей.
C 2018 6.7.2.1 10 говорит:
Заметьте, что это не является специфическим для типа - это некоторый целочисленный тип - и в нем не говорится, что тип - это тип, который использовался для объявления битового поля, как
uint64_t a : 1;
показано в вопросе. Это, очевидно, оставляет открытой для реализации возможность выбора типа.Проект C ++ 2017 n4659 12.2.4 [class.bit] 1 говорит о объявлении битового поля:
Это подразумевает, что в объявлении, таком как
uint64_t a : 1;
,: 1
не является частью типа члена классаa
, поэтому тип, как если бы он былuint64_t a;
, и, следовательно, типa
isuint64_t
.Таким образом, похоже, что GCC рассматривает битовое поле в C как некоторый целочисленный тип 32-битный или более узкий, если оно подходит, и битовое поле в C ++ как его объявленный тип, и это, похоже, не нарушает стандарты.
источник
E1
в этом случае это 52-битное битовое поле.uint64_t a : 33
множество 2 ^ 33-1 в структуреs
, а затем, в реализации C с 32-битнойint
,s.a+s.a
должно дать 2 ^ 33-2 за счет упаковки, но Clang производит 2 ^ 34- 2; это, очевидно, относится к этому какuint64_t
.s.a+s.a
обычные арифметические преобразования не изменят типs.a
, поскольку он ширеunsigned int
, поэтому арифметика будет выполняться в 33-битном типе.)uint64_t
. Если это 64-битная компиляция, похоже, что Clang согласуется с тем, как GCC обрабатывает 64-битные компиляции, не урезая. Clang по-разному относится к 32- и 64-битным компиляциям? (И, кажется, я только что узнал еще одну причину, чтобы избегать битовых полей ...)-m32
и-m64
с предупреждением, что тип является расширением GCC. В Apple Clang 11.0 у меня нет библиотек для запуска 32-битного кода, но сгенерированная сборка показываетpushl $3
иpushl $-2
перед вызовомprintf
, поэтому я думаю, что это 2 ^ 34−2. Таким образом, Apple Clang не отличается между 32-разрядными и 64-разрядными целевыми объектами, но со временем изменился.Эндрю Хенле предложил строгое толкование Стандарта Си: тип битового поля - это целочисленный тип со знаком или без знака с точно указанной шириной.
Вот тест, который поддерживает эту интерпретацию: используя
_Generic()
конструкцию C1x , я пытаюсь определить тип битовых полей различной ширины. Я должен был определить их с типом,long long int
чтобы избежать предупреждений при компиляции с Clang.Вот источник:
Вот вывод программы, скомпилированный с 64-битным Clang:
Кажется, что все битовые поля имеют определенный тип, а не тип, определенный для определенной ширины.
Вот вывод программы, скомпилированный с 64-битным gcc:
Что согласуется с каждой шириной, имеющей различный тип.
Выражение
E1 << E2
имеет тип продвигаемого левого операнда, поэтому любая ширина меньше, чемINT_WIDTH
продвигается сint
помощью целочисленного продвижения, и любая ширина больше, чемINT_WIDTH
остается одна. Результат выражения действительно должен быть усечен до ширины битового поля, если эта ширина больше, чемINT_WIDTH
. Точнее, он должен быть усечен для неподписанного типа и может быть реализацией, определенной для подписанных типов.То же самое должно произойти и для
E1 + E2
других арифметических операторов, еслиE1
илиE2
являются битовыми полями с шириной, большей, чем уint
. Операнд с меньшей шириной преобразуется в тип с большей шириной, и результат также имеет тип type. Это очень нелогичное поведение, вызывающее много неожиданных результатов, может быть причиной широко распространенного убеждения, что битовые поля являются поддельными и их следует избегать.Многие компиляторы, похоже, не следуют этой интерпретации Стандарта С, и эта интерпретация не очевидна из текущей формулировки. Было бы полезно уточнить семантику арифметических операций с операндами битового поля в будущей версии стандарта C.
источник
int
может представлять все значения исходного типа (как ограничено шириной для битового поля), значение преобразуется в aint
; в противном случае оно превращают вunsigned int
Их называют целые акции.. - §6.3.1.8 , §6.7.2.1 ), не охватывают случай , когда ширина битового поля шире , чемint
.int
,unsigned int
и_Bool
.int
формате и не должны быть фиксированными 32.uint64_t
битовых полей, стандарт не должен ничего о них говорить - это должно быть отражено в документации реализации определенных в реализации частей поведения. битовых полей. В частности, то, что 52-битное поле битов не помещается в (32-битное),int
это не должно означать, что они сокращены до 32-битногоunsigned int
, но это буквальное чтение 6.3. 1.1 говорит.Кажется, проблема связана с 32-битным генератором кода gcc в режиме C:
Вы можете сравнить код сборки с помощью компилятора Godbolt.
Вот исходный код этого теста:
Выход в режиме C (флаги
-xc -O2 -m32
)Проблема заключается в последней инструкции,
and edx, 1048575
которая обрезает 12 самых значимых битов.Вывод в режиме C ++ идентичен за исключением последней инструкции:
Вывод в 64-битном режиме намного проще и корректнее, но отличается для компиляторов C и C ++:
Вы должны отправить отчет об ошибке на gcc tracker.
источник