Truncamiento inconsistente de expresiones enteras de campo de bits sin signo entre C ++ y C en diferentes compiladores

10

Edición 2 :

Estaba depurando un extraño error de prueba cuando una función que anteriormente residía en un archivo fuente C ++ pero se movió literalmente a un archivo C, comenzó a devolver resultados incorrectos. El MVE a continuación permite reproducir el problema con GCC. Sin embargo, cuando, por capricho, compilé el ejemplo con Clang (y luego con VS), ¡obtuve un resultado diferente! No puedo determinar si tratar esto como un error en uno de los compiladores, o como una manifestación de resultado indefinido permitido por el estándar C o C ++. Curiosamente, ninguno de los compiladores me dio ninguna advertencia sobre la expresión.

El culpable es esta expresión:

ctl.b.p52 << 12;

Aquí, p52se escribe como uint64_t; También es parte de una unión (ver control_tabajo). La operación de cambio no pierde ningún dato ya que el resultado todavía cabe en 64 bits. Sin embargo, ¡entonces GCC decide truncar el resultado a 52 bits si uso el compilador de C ! Con el compilador de C ++, se conservan los 64 bits de resultado.

Para ilustrar esto, el siguiente programa de ejemplo compila dos funciones con cuerpos idénticos y luego compara sus resultados. c_behavior()se coloca en un archivo fuente C y cpp_behavior()en un archivo C ++, y main()hace la comparación.

Repositorio con el código de ejemplo: https://github.com/grigory-rechistov/c-cpp-bitfields

El encabezado common.h define una unión de campos de bits anchos de 64 bits y enteros y declara dos funciones:

#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

Las funciones tienen cuerpos idénticos, excepto que uno se trata como C y otro 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;
}

C Principal:

#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 muestra la diferencia entre los resultados que devuelven:

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

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

Sin embargo, con Clang C y C ++ se comportan de manera idéntica y como se esperaba:

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

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

Con Visual Studio obtengo el mismo resultado que con 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++

Probé los ejemplos en Windows, aunque el problema original con GCC se descubrió en Linux.

Grigory Rechistov
fuente
1
los campos de bits son notoriamente falsos para anchos grandes. Encontré problemas similares en esta pregunta: stackoverflow.com/questions/58846584/…
chqrlie
@chqrlie leí el C <<operador que requiere el truncamiento.
Andrew Henle
Publique un stackoverflow.com/help/minimal-reproducible-example . El código actual no tiene main.cy probablemente causa un comportamiento indefinido de varias maneras. En mi opinión, sería más claro publicar una MRE de un solo archivo que produce una salida diferente cuando se compila con cada compilador. Porque la interoperabilidad C-C ++ no está bien especificada por el estándar. También tenga en cuenta que el alias de unión causa UB en C ++.
MM
@ MM Correcto, se resbaló cuando estaba publicando la pregunta. Lo he agregado ahora, y también creo que tener un pequeño repositorio con él también podría ser una idea
Grigory Rechistov
@MM "OMI sería más claro publicar un MRE de un solo archivo que produce una salida diferente cuando se compila con cada compilador." No he pensado en eso ya que estaba transformando mi código de producción en algo más pequeño, pero debería ser posible reformular el reproductor en un solo archivo.
Grigory Rechistov

Respuestas:

6

C y C ++ tratan los tipos de miembros de campo de bits de manera diferente.

C 2018 6.7.2.1 10 dice:

Se interpreta que un campo de bits tiene un tipo entero con signo o sin signo que consta del número especificado de bits ...

Observe que esto no es específico sobre el tipo (es un tipo entero) y no dice que el tipo es el tipo que se utilizó para declarar el campo de bits, como se uint64_t a : 1;muestra en la pregunta. Esto aparentemente lo deja abierto a la implementación para elegir el tipo.

C ++ 2017 draft n4659 12.2.4 [class.bit] 1 dice, de una declaración de campo de bits:

... El atributo de campo de bits no es parte del tipo del miembro de la clase ...

Esto implica que, en una declaración como uint64_t a : 1;, el : 1no es parte del tipo del miembro de la clase a, por lo que el tipo es como si lo fuera uint64_t a;, y por lo tanto el tipo de aes uint64_t.

Por lo tanto, parece que GCC trata un campo de bits en C como un tipo entero de 32 bits o más estrecho si cabe y un campo de bits en C ++ como su tipo declarado, y esto no parece violar los estándares.

Eric Postpischil
fuente
Leí el truncamiento en C como obligatorio según 6.5.7 4 (la redacción de C18 es similar): "El resultado de E1 << E2 es E1 posiciones de bit E2 desplazadas a la izquierda; los bits vacantes se rellenan con ceros. Si E1 tiene un tipo sin signo , el valor del resultado es E1 x 2E2, módulo reducido uno más que el valor máximo representable en el tipo de resultado ". E1en este caso es un campo de bits de 52 bits.
Andrew Henle
@ AndrewHenle: veo lo que estás diciendo. El tipo de un n campo de bits bit es “ n número entero bits” (despreciando de signo por ahora). Lo estaba interpretando como el tipo de un campo de bits de n bits es algún tipo de entero, que la implementación elige. Basado únicamente en la redacción de 6.7.2.1 10, estoy a favor de su interpretación. Pero un problema con eso es que, dado un uint64_t a : 33conjunto de 2 ^ 33−1 en una estructura s, entonces, en una implementación de C con 32 bits int, s.a+s.adebería producir 2 ^ 33−2 debido al ajuste, pero Clang produce 2 ^ 34− 2; aparentemente lo trata como uint64_t.
Eric Postpischil
@AndrewHenle: (Más sobre el razonamiento: en s.a+s.a, las conversiones aritméticas habituales no cambiarían el tipo de s.a, ya que es más ancho que unsigned int, por lo que la aritmética se haría en el tipo de 33 bits.)
Eric Postpischil
pero Clang produce 2 ^ 34−2; aparentemente lo trata como uint64_t. Si se trata de una compilación de 64 bits, parece que Clang es coherente con la forma en que GCC trata las compilaciones de 64 bits al no truncar. ¿Clang trata las compilaciones de 32 y 64 bits de manera diferente? (Y parece que acabo de aprender otra razón para evitar campos de bits ...)
Andrew Henle
@AndrewHenle: Bueno, el viejo Apple Clang 1.7 produce 2 ^ 32−2 (no 2 ^ 33−2; ¡perdió un poco!) Con -m32y -m64, con una advertencia de que el tipo es una extensión GCC. Con Apple Clang 11.0, no tengo bibliotecas para ejecutar código de 32 bits, pero el ensamblado generado se muestra pushl $3y pushl $-2antes de llamar printf, así que creo que es 2 ^ 34−2. Por lo tanto, Apple Clang no difiere entre los objetivos de 32 bits y 64 bits, pero cambió con el tiempo.
Eric Postpischil
4

Andrew Henle sugirió una interpretación estricta del Estándar C: el tipo de un campo de bits es un tipo entero con signo o sin signo con exactamente el ancho especificado.

Aquí hay una prueba que respalda esta interpretación: usando la _Generic()construcción C1x , estoy tratando de determinar el tipo de campos de bits de diferentes anchos. Tuve que definirlos con el tipo long long intpara evitar advertencias al compilar con clang.

Aquí está la fuente:

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

Aquí está la salida del programa compilada con sonido 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 los campos de bits parecen tener el tipo definido en lugar de un tipo específico para el ancho definido.

Aquí está la salida del programa compilada con 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

Lo cual es consistente con cada ancho que tiene un tipo diferente.

La expresión E1 << E2tiene el tipo del operando izquierdo promovido, por lo que cualquier ancho menor que el que INT_WIDTHse promueve a inttravés de la promoción de enteros y cualquier ancho mayor que el que INT_WIDTHse deja solo. De hecho, el resultado de la expresión debería truncarse al ancho del campo de bits si este ancho es mayor que INT_WIDTH. Más precisamente, debería truncarse para un tipo sin signo y podría ser una implementación definida para tipos con signo.

Lo mismo debería ocurrir para E1 + E2y otros operadores aritméticos si E1o E2son campos de bits con un ancho mayor que el de int. El operando con el ancho más pequeño se convierte al tipo con el ancho más grande y el resultado también tiene el tipo de tipo. Este comportamiento muy intuitivo que causa muchos resultados inesperados, puede ser la causa de la creencia generalizada de que los campos de bits son falsos y deben evitarse.

Muchos compiladores no parecen seguir esta interpretación del Estándar C, ni esta interpretación es obvia a partir de la redacción actual. Sería útil aclarar la semántica de las operaciones aritméticas que involucran operandos de campo de bits en una versión futura del Estándar C.

chqrlie
fuente
1
Creo que el término clave es "promociones enteras". La discusión de campos de bits con promociones de enteros (C11 §6.3.1.1 - Si un intpuede representar todos los valores del tipo original (restringido por el ancho, para un campo de bits), el valor se convierte en un int; de lo contrario, se convierte en unsigned intA. Estas se llaman promociones de enteros. - §6.3.1.8 , §6.7.2.1 ), no cubra el caso donde el ancho de un campo de bits es más ancho que an int.
Jonathan Leffler
1
No ayuda que el estándar deje indefinidos (en el mejor de los casos, definidos por la implementación) qué tipos están permitidos para campos de bits que no sean int, unsigned inty _Bool.
Jonathan Leffler
1
"cualquier ancho menor que 32", "cualquier ancho mayor que 32" y "si este ancho es mayor que 32" presumiblemente debería reflejar el número de bits en plano inty no ser un 32 fijo.
Ben Voigt
1
Estoy de acuerdo en que hay un problema (de supervisión) en el estándar C. Puede haber espacio para argumentar que, dado que el estándar no sanciona el uso de uint64_tcampos de bits, el estándar no tiene que decir nada sobre ellos; debe estar cubierto por la documentación de la implementación de las partes del comportamiento definidas por la implementación de campos de bits. En particular, solo porque los 52 bits del campo de bits no caben en un (32 bits) intno debería significar que están agrupados en 32 bits unsigned int, pero eso es lo que es una lectura literal de 6.3. 1.1 dice.
Jonathan Leffler
1
Además, si C ++ ha resuelto explícitamente los problemas de 'gran campo de bits', entonces C debería seguir esa guía lo más cerca posible, a menos que haya algo inherentemente específico para C ++ sobre esa resolución (lo cual no es probable).
Jonathan Leffler
2

El problema parece ser específico del generador de código de 32 bits de gcc en modo C:

Puede comparar el código de ensamblaje utilizando el Explorador de compiladores de Godbolt

Aquí está el código fuente de esta prueba:

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

La salida en modo C (banderas -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

El problema es la última instrucción and edx, 1048575que recorta los 12 bits más significativos.

La salida en modo C ++ es idéntica excepto por la última instrucción:

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

La salida en modo de 64 bits es mucho más simple y correcta, pero diferente para los compiladores C y C ++:

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

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

Debe presentar un informe de error en el rastreador de errores de gcc.

chqrlie
fuente
Mis experimentos fueron solo para objetivos de 64 bits, pero su caso de 32 bits es aún más extraño. Supongo que se debe un informe de error. Primero, necesito volver a verificarlo en la última versión de GCC disponible para mí.
Grigory Rechistov
1
@GrigoryRechistov Dada la redacción en el estándar C , el error puede muy bien ser el objetivo de 64 bits que no puede truncar el resultado a 52 bits. Yo personalmente lo vería de esa manera.
Andrew Henle