Несовместимое усечение беззнаковых целочисленных выражений битового поля между C ++ и C в разных компиляторах

10

Изменить 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.

Григорий Речистов
источник
1
битовые поля, как известно, являются поддельными для больших ширин. Я сталкивался с подобными проблемами в этом вопросе: stackoverflow.com/questions/58846584/…
chqrlie
@chqrlie Я читаю оператор C<< как требующий усечения.
Эндрю Хенле
Пожалуйста, разместите stackoverflow.com/help/minimal-reproducible-example . Текущий код не имеет main.cи, вероятно, вызывает неопределенное поведение несколькими способами. IMO было бы яснее опубликовать MRE с одним файлом, который выдает разные результаты при компиляции с каждым компилятором. Потому что взаимодействие C-C ++ не определено стандартом. Также обратите внимание, что объединение псевдонимов вызывает UB в C ++.
ММ
@ ММ Правильно, это ускользнуло, когда я отправил вопрос. Я добавил это сейчас, и я также думаю, что иметь небольшой репозиторий с ним также может быть идеей
Григорий Речистов
@MM «IMO, было бы яснее опубликовать однофайловую MRE, которая выдает различный вывод при компиляции с каждым компилятором. переформулировать репродуктор в один файл.
Григорий Речистов

Ответы:

6

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;, и, следовательно, тип ais uint64_t.

Таким образом, похоже, что GCC рассматривает битовое поле в C как некоторый целочисленный тип 32-битный или более узкий, если оно подходит, и битовое поле в C ++ как его объявленный тип, и это, похоже, не нарушает стандарты.

Эрик Постпищил
источник
Я читаю усечение в C как обязательное для 6.5.7 4 (формулировка C18 аналогична): «Результатом E1 << E2 является E1 смещенная влево позиция E2; свободные биты заполнены нулями. Если E1 имеет тип без знака , значение результата равно E1 x 2E2, уменьшено по модулю на единицу больше, чем максимальное значение, представляемое в типе результата ". E1в этом случае это 52-битное битовое поле.
Эндрю Хенле
@AndrewHenle: Я понимаю, что вы говорите. Тип n- битного битового поля - « n- битное целое» (пока пренебрегая подписью). Я интерпретировал это как тип n- битного битового поля - это некий целочисленный тип, который выбирает реализация. Основываясь исключительно на формулировке в 6.7.2.1 10, я поддерживаю вашу интерпретацию. Но проблема в том, что, имея uint64_t a : 33множество 2 ^ 33-1 в структуре s, а затем, в реализации C с 32-битной int, s.a+s.aдолжно дать 2 ^ 33-2 за счет упаковки, но Clang производит 2 ^ 34- 2; это, очевидно, относится к этому как uint64_t.
Эрик Постпишил
@AndrewHenle: (Подробнее об этом: s.a+s.aобычные арифметические преобразования не изменят тип s.a, поскольку он шире unsigned int, поэтому арифметика будет выполняться в 33-битном типе.)
Эрик Постпишил
но Clang производит 2 ^ 34-2; это, очевидно, относится к этому как uint64_t. Если это 64-битная компиляция, похоже, что Clang согласуется с тем, как GCC обрабатывает 64-битные компиляции, не урезая. Clang по-разному относится к 32- и 64-битным компиляциям? (И, кажется, я только что узнал еще одну причину, чтобы избегать битовых полей ...)
Эндрю Хенле
@AndrewHenle: Ну, старый Apple Clang 1.7 производит 2 ^ 32-2 (не 2 ^ 33-2; он немного потерял!) Как с, так -m32и -m64с предупреждением, что тип является расширением GCC. В Apple Clang 11.0 у меня нет библиотек для запуска 32-битного кода, но сгенерированная сборка показывает pushl $3и pushl $-2перед вызовом printf, поэтому я думаю, что это 2 ^ 34−2. Таким образом, Apple Clang не отличается между 32-разрядными и 64-разрядными целевыми объектами, но со временем изменился.
Эрик Постпишил
4

Эндрю Хенле предложил строгое толкование Стандарта Си: тип битового поля - это целочисленный тип со знаком или без знака с точно указанной шириной.

Вот тест, который поддерживает эту интерпретацию: используя _Generic()конструкцию C1x , я пытаюсь определить тип битовых полей различной ширины. Я должен был определить их с типом, long long intчтобы избежать предупреждений при компиляции с Clang.

Вот источник:

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

Вот вывод программы, скомпилированный с 64-битным Clang:

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

Кажется, что все битовые поля имеют определенный тип, а не тип, определенный для определенной ширины.

Вот вывод программы, скомпилированный с 64-битным gcc:

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

Что согласуется с каждой шириной, имеющей различный тип.

Выражение E1 << E2имеет тип продвигаемого левого операнда, поэтому любая ширина меньше, чем INT_WIDTHпродвигается с intпомощью целочисленного продвижения, и любая ширина больше, чем INT_WIDTHостается одна. Результат выражения действительно должен быть усечен до ширины битового поля, если эта ширина больше, чем INT_WIDTH. Точнее, он должен быть усечен для неподписанного типа и может быть реализацией, определенной для подписанных типов.

То же самое должно произойти и для E1 + E2других арифметических операторов, если E1или E2являются битовыми полями с шириной, большей, чем у int. Операнд с меньшей шириной преобразуется в тип с большей шириной, и результат также имеет тип type. Это очень нелогичное поведение, вызывающее много неожиданных результатов, может быть причиной широко распространенного убеждения, что битовые поля являются поддельными и их следует избегать.

Многие компиляторы, похоже, не следуют этой интерпретации Стандарта С, и эта интерпретация не очевидна из текущей формулировки. Было бы полезно уточнить семантику арифметических операций с операндами битового поля в будущей версии стандарта C.

chqrlie
источник
1
Я думаю, что ключевым термином является «целочисленное продвижение». Обсуждение битовых полей с целочисленными преобразованиями (C11 §6.3.1.1 - Если an intможет представлять все значения исходного типа (как ограничено шириной для битового поля), значение преобразуется в a int; в противном случае оно превращают в unsigned intИх называют целые акции.. - §6.3.1.8 , §6.7.2.1 ), не охватывают случай , когда ширина битового поля шире , чем int.
Джонатан Леффлер
1
Не помогает то, что стандарт оставляет неопределенным (в лучшем случае определяемым реализацией), какие типы разрешены для битовых полей, кроме int, unsigned intи _Bool.
Джонатан Леффлер
1
«любая ширина меньше 32», «любая ширина больше 32» и «если эта ширина больше 32», по-видимому, должны отражать число битов в обычном intформате и не должны быть фиксированными 32.
Бен Фойгт,
1
Я согласен, что в стандарте C есть проблема (недосмотра). Возможно, есть основания утверждать, что, поскольку стандарт не разрешает использование uint64_tбитовых полей, стандарт не должен ничего о них говорить - это должно быть отражено в документации реализации определенных в реализации частей поведения. битовых полей. В частности, то, что 52-битное поле битов не помещается в (32-битное), intэто не должно означать, что они сокращены до 32-битного unsigned int, но это буквальное чтение 6.3. 1.1 говорит.
Джонатан Леффлер
1
Кроме того, если C ++ разрешил проблемы «больших битовых полей» явно, то C должен следовать этому примеру как можно точнее - если только в этом разрешении нет ничего специфического для C ++ (что маловероятно).
Джонатан Леффлер
2

Кажется, проблема связана с 32-битным генератором кода gcc в режиме C:

Вы можете сравнить код сборки с помощью компилятора Godbolt.

Вот исходный код этого теста:

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

Выход в режиме C (флаги -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

Проблема заключается в последней инструкции, and edx, 1048575которая обрезает 12 самых значимых битов.

Вывод в режиме C ++ идентичен за исключением последней инструкции:

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

Вывод в 64-битном режиме намного проще и корректнее, но отличается для компиляторов C и C ++:

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

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

Вы должны отправить отчет об ошибке на gcc tracker.

chqrlie
источник
Мои эксперименты были только для 64-битных целей, но ваш 32-битный случай еще более странный. Я думаю, что сообщение об ошибке из-за. Во-первых, мне нужно перепроверить его на последней доступной мне версии GCC.
Григорий Речистов
1
@GrigoryRechistov Учитывая формулировку в стандарте C , ошибкой может быть 64-битная цель, которая не усекает результат до 52 бит. Я лично смотрю на это так.
Эндрю Хенле