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í, p52
se escribe como uint64_t
; También es parte de una unión (ver control_t
abajo). 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.
fuente
<<
operador que requiere el truncamiento.main.c
y 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 ++.Respuestas:
C y C ++ tratan los tipos de miembros de campo de bits de manera diferente.
C 2018 6.7.2.1 10 dice:
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:
Esto implica que, en una declaración como
uint64_t a : 1;
, el: 1
no es parte del tipo del miembro de la clasea
, por lo que el tipo es como si lo fuerauint64_t a;
, y por lo tanto el tipo dea
esuint64_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.
fuente
E1
en este caso es un campo de bits de 52 bits.uint64_t a : 33
conjunto de 2 ^ 33−1 en una estructuras
, entonces, en una implementación de C con 32 bitsint
,s.a+s.a
debería producir 2 ^ 33−2 debido al ajuste, pero Clang produce 2 ^ 34− 2; aparentemente lo trata comouint64_t
.s.a+s.a
, las conversiones aritméticas habituales no cambiarían el tipo des.a
, ya que es más ancho queunsigned int
, por lo que la aritmética se haría en el tipo de 33 bits.)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 ...)-m32
y-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 muestrapushl $3
ypushl $-2
antes de llamarprintf
, 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.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 tipolong long int
para evitar advertencias al compilar con clang.Aquí está la fuente:
Aquí está la salida del programa compilada con sonido de 64 bits:
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:
Lo cual es consistente con cada ancho que tiene un tipo diferente.
La expresión
E1 << E2
tiene el tipo del operando izquierdo promovido, por lo que cualquier ancho menor que el queINT_WIDTH
se promueve aint
través de la promoción de enteros y cualquier ancho mayor que el queINT_WIDTH
se deja solo. De hecho, el resultado de la expresión debería truncarse al ancho del campo de bits si este ancho es mayor queINT_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 + E2
y otros operadores aritméticos siE1
oE2
son campos de bits con un ancho mayor que el deint
. 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.
fuente
int
puede representar todos los valores del tipo original (restringido por el ancho, para un campo de bits), el valor se convierte en unint
; de lo contrario, se convierte enunsigned int
A. 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 anint
.int
,unsigned int
y_Bool
.int
y no ser un 32 fijo.uint64_t
campos 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)int
no debería significar que están agrupados en 32 bitsunsigned int
, pero eso es lo que es una lectura literal de 6.3. 1.1 dice.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:
La salida en modo C (banderas
-xc -O2 -m32
)El problema es la última instrucción
and edx, 1048575
que recorta los 12 bits más significativos.La salida en modo C ++ es idéntica excepto por la última instrucción:
La salida en modo de 64 bits es mucho más simple y correcta, pero diferente para los compiladores C y C ++:
Debe presentar un informe de error en el rastreador de errores de gcc.
fuente