Truncamento inconsistente de expressões inteiras de campo de bits não assinadas entre C ++ e C em diferentes compiladores

10

Edição 2 :

Eu estava depurando uma falha de teste estranha quando uma função que residia anteriormente em um arquivo de origem C ++, mas foi movida para um arquivo C literalmente, começou a retornar resultados incorretos. O MVE abaixo permite reproduzir o problema com o GCC. No entanto, quando eu, por um capricho, compilei o exemplo com Clang (e mais tarde com o VS), obtive um resultado diferente! Não consigo descobrir se devo tratar isso como um bug em um dos compiladores ou como manifestação de resultado indefinido permitido pelo padrão C ou C ++. Estranhamente, nenhum dos compiladores me deu nenhum aviso sobre a expressão.

O culpado é esta expressão:

ctl.b.p52 << 12;

Aqui, p52é digitado como uint64_t; também faz parte de um sindicato (veja control_tabaixo). A operação de deslocamento não perde dados, pois o resultado ainda se encaixa em 64 bits. No entanto, o GCC decide truncar o resultado para 52 bits se eu usar o compilador C ! Com o compilador C ++, todos os 64 bits de resultado são preservados.

Para ilustrar isso, o programa de exemplo abaixo compila duas funções com corpos idênticos e depois compara seus resultados. c_behavior()é colocado em um arquivo de origem C e cpp_behavior()em um arquivo C ++ e main()faz a comparação.

Repositório com o código de exemplo: https://github.com/grigory-rechistov/c-cpp-bitfields

O cabeçalho common.h define uma união de campos de bits e números inteiros de 64 bits e declara duas funções:

#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

As funções têm corpos idênticos, exceto que um é tratado como C e outro como 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;
}

O GCC mostra a diferença entre os resultados que eles retornam:

$ gcc -Wpedantic main.c c-part.c cpp-part.cpp

$ ./a.exe
OMG C != C++

No entanto, com Clang C e C ++ se comportam de forma idêntica e conforme o esperado:

$ clang -Wpedantic main.c c-part.c cpp-part.cpp

$ ./a.exe
C == C++

Com o Visual Studio, obtenho o mesmo resultado que com o 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++

Eu tentei os exemplos no Windows, mesmo que o problema original com o GCC tenha sido descoberto no Linux.

Grigory Rechistov
fonte
11
Os campos de bits são notoriamente falsos para grandes larguras. Me deparei com problemas semelhantes nesta pergunta: stackoverflow.com/questions/58846584/…
chqrlie
@chqrlie Eu li o operador C<< como exigindo o truncamento.
Andrew Henle
Publique um stackoverflow.com/help/minimal-reproducible-example . O código atual não possui main.ce provavelmente causa comportamento indefinido de várias maneiras. Na IMO, seria mais claro publicar um MRE de arquivo único que produza saída diferente quando compilado com cada compilador. Porque a interoperabilidade C-C ++ não é bem especificada pelo padrão. Observe também que o alias de união causa UB em C ++.
MM
@ MM Certo, ele caiu quando eu estava postando a pergunta. Eu o adicionei agora e também acho que ter um pequeno repositório com ele também pode ser uma ideia
Grigory Rechistov
@MM "OMI seria mais claro postar um MRE de arquivo único que produz saída diferente quando compilado com cada compilador." Eu não pensei nisso enquanto estava transformando meu código de produção em algo menor, mas deveria ser possível reformule o reprodutor em um único arquivo.
Grigory Rechistov 18/03

Respostas:

6

C e C ++ tratam os tipos de membros de campo de bits de maneira diferente.

C 2018 6.7.2.1 10 diz:

Um campo de bits é interpretado como tendo um tipo inteiro assinado ou não assinado, consistindo no número especificado de bits…

Observe que isso não é específico sobre o tipo - é algum tipo de número inteiro - e não diz que o tipo é o tipo que foi usado para declarar o campo de bits, como no uint64_t a : 1; mostrado na pergunta. Aparentemente, isso deixa aberto a implementação para escolher o tipo.

O rascunho do C ++ 2017 n4659 12.2.4 [class.bit] 1 diz que uma declaração de campo de bit:

… O atributo bit-field não faz parte do tipo de membro da classe…

Isso implica que, em uma declaração como uint64_t a : 1;, : 1não faz parte do tipo do membro da classe a, portanto, o tipo é como se fosse uint64_t a;e, portanto, o tipo de aéuint64_t .

Portanto, parece que o GCC trata um campo de bits em C como um número inteiro de 32 bits ou mais estreito, se for o caso, e um campo de bits em C ++ como seu tipo declarado, e isso não parece violar os padrões.

Eric Postpischil
fonte
Eu li o truncamento em C como obrigatório por 6.5.7 4 (o texto C18 é semelhante): "O resultado de E1 << E2 é E1 com posições de bits E2 com desvio à esquerda; os bits desocupados são preenchidos com zeros. Se E1 tiver um tipo não assinado , o valor do resultado é E1 x 2E2, módulo reduzido um a mais que o valor máximo representável no tipo de resultado ". E1neste caso, é um campo de bits de 52 bits.
Andrew Henle
@AndrewHenle: Entendo o que você está dizendo. O tipo de um campo de bits de n bits é " número inteiro de n bits" (negligenciando a assinatura por enquanto). Eu estava interpretando como o tipo de um campo de bits de n bits é algum tipo inteiro, que a implementação escolhe. Baseado apenas no texto do 6.7.2.1 10, sou a favor da sua interpretação. Mas um problema disso é que, dado um uint64_t a : 33conjunto para 2 ^ 33−1 em uma estrutura s, então, em uma implementação C com 32 bits int, s.a+s.adeve render 2 ^ 33−2 devido ao empacotamento, mas Clang produz 2 ^ 34− 2; aparentemente o trata como uint64_t.
Eric Postpischil 17/03
@AndrewHenle: (Mais sobre o raciocínio: em s.a+s.a, as conversões aritméticas comuns não mudariam o tipo de s.a, uma vez que é mais largo do que unsigned int, portanto, a aritmética seria feita no tipo de 33 bits.)
Eric Postpischil
mas Clang produz 2 ^ 34−2; aparentemente o trata como uint64_t. Se essa é uma compilação de 64 bits, isso parece tornar o Clang consistente com o modo como o GCC está tratando as compilações de 64 bits por não truncar. O Clang trata as compilações de 32 e 64 bits de maneira diferente? (E parece que aprendi outro motivo para evitar campos de bits ...)
Andrew Henle
@AndrewHenle: Bem, o antigo Apple Clang 1.7 produz 2 ^ 32-2 (não 2 ^ 33-2; perdeu um pouco!) Tanto com -m32quanto -m64com um aviso de que o tipo é uma extensão do GCC. Com o Apple Clang 11.0, não tenho bibliotecas para executar código de 32 bits, mas o assembly gerado é exibido pushl $3e pushl $-2antes da chamada printf, então acho que isso é 2 ^ 34-2. Portanto, o Apple Clang não difere entre os destinos de 32 e 64 bits, mas mudou com o tempo.
Eric Postpischil 17/03
4

Andrew Henle sugeriu uma interpretação estrita do padrão C: o tipo de um campo de bits é um tipo inteiro assinado ou não assinado, com exatamente a largura especificada.

Aqui está um teste que suporta essa interpretação: usando a _Generic()construção C1x , estou tentando determinar o tipo de campo de bits de diferentes larguras. Eu tive que defini-los com o tipo long long intpara evitar avisos ao compilar com clang.

Aqui está a fonte:

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

Aqui está a saída do programa compilada com clang de 64 bits:

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

Todos os campos de bits parecem ter o tipo definido em vez de um tipo específico para a largura definida.

Aqui está a saída do programa compilada com o gcc de 64 bits:

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

O que é consistente com cada largura tendo um tipo diferente.

A expressão E1 << E2tem o tipo do operando esquerdo promovido, portanto, qualquer largura menor que INT_WIDTHé promovida para intvia promoção inteira e qualquer largura maior que INT_WIDTHé deixada sozinha. O resultado da expressão deve realmente ser truncado para a largura do campo de bits se essa largura for maior queINT_WIDTH . Mais precisamente, ele deve ser truncado para um tipo não assinado e pode ser uma implementação definida para tipos assinados.

O mesmo deve ocorrer para E1 + E2e outros operadores aritméticos se E1ou E2são campos de bit, com uma largura maior do que a da int. O operando com a largura menor é convertido para o tipo com a largura maior e o resultado também tem o tipo de tipo. Esse comportamento muito contra-intuitivo, que causa muitos resultados inesperados, pode ser a causa da crença generalizada de que os campos de bits são falsos e devem ser evitados.

Muitos compiladores parecem não seguir essa interpretação do Padrão C, nem é óbvia na interpretação atual. Seria útil esclarecer a semântica de operações aritméticas envolvendo operandos de campo de bits em uma versão futura do C Standard.

chqrlie
fonte
11
Eu acho que o termo-chave é 'promoções inteiras'. A discussão de campos de bits com promoções de número inteiro (C11 §6.3.1.1 - Se um intpuder representar todos os valores do tipo original (conforme restrito pela largura, para um campo de bits), o valor será convertido em um int; caso contrário, ele é convertido em um unsigned int. Estes são chamados de promoções de número inteiro - §6.3.1.8 , §6.7.2.1 ), não cobrem o caso em que a largura de um campo de bit é maior que um int.
Jonathan Leffler
11
Não ajuda que o padrão deixe indefinido (na melhor das hipóteses definido pela implementação) que tipos são permitidos para campos de bits diferentes de int, unsigned inte _Bool.
Jonathan Leffler
11
"qualquer largura menor que 32", "qualquer largura maior que 32" e "se essa largura for maior que 32" provavelmente devem refletir o número de bits em forma simples inte não devem ser fixos 32.
Ben Voigt
11
Concordo que há um problema (de supervisão) no padrão C. Pode haver espaço para argumentar que, como o padrão não sanciona o uso de uint64_tcampos de bits, o padrão não precisa dizer nada sobre eles - deve ser coberto pela documentação da implementação das partes do comportamento definidas pela implementação de campos de bits. Em particular, apenas porque os 52 bits do campo de bits não se encaixam em um (32 bits), intisso não significa que eles estão agrupados em 32 bits unsigned int, mas é isso que uma leitura literal de 6.3. 1.1 diz.
Jonathan Leffler
11
Além disso, se o C ++ tiver resolvido problemas de "grande campo de bits" explicitamente, o C deverá seguir essa liderança o mais próximo possível - a menos que exista algo inerentemente específico do C ++ nessa resolução (o que não é provável).
Jonathan Leffler
2

O problema parece ser específico ao gerador de código de 32 bits do gcc no modo C:

Você pode comparar o código de montagem usando o Compiler Explorer de Godbolt

Aqui está o código fonte para este teste:

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

A saída no modo C (sinalizadores -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

O problema é a última instrução and edx, 1048575que corta os 12 bits mais significativos.

A saída no modo C ++ é idêntica, exceto pela última instrução:

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

A saída no modo de 64 bits é muito mais simples e correta, mas diferente para os compiladores C e C ++:

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

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

Você deve registrar um relatório de bug no rastreador de erros do gcc.

chqrlie
fonte
Minhas experiências foram apenas para destinos de 64 bits, mas o seu caso de 32 bits é ainda mais bizarro. Eu acho que é devido um relatório de bug. Primeiro, preciso verificar novamente a versão mais recente do GCC disponível para mim.
Grigory Rechistov 17/03
11
@GrigoryRechistov Dada a redação do padrão C , o bug pode muito bem ser o destino de 64 bits que falha ao truncar o resultado para 52 bits. Eu pessoalmente veria dessa maneira.
Andrew Henle