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_t
abaixo). 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.
fonte
<<
como exigindo o truncamento.main.c
e 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 ++.Respostas:
C e C ++ tratam os tipos de membros de campo de bits de maneira diferente.
C 2018 6.7.2.1 10 diz:
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:
Isso implica que, em uma declaração como
uint64_t a : 1;
,: 1
não faz parte do tipo do membro da classea
, portanto, o tipo é como se fosseuint64_t a;
e, portanto, o tipo dea
é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.
fonte
E1
neste caso, é um campo de bits de 52 bits.uint64_t a : 33
conjunto para 2 ^ 33−1 em uma estruturas
, então, em uma implementação C com 32 bitsint
,s.a+s.a
deve render 2 ^ 33−2 devido ao empacotamento, mas Clang produz 2 ^ 34− 2; aparentemente o trata comouint64_t
.s.a+s.a
, as conversões aritméticas comuns não mudariam o tipo des.a
, uma vez que é mais largo do queunsigned int
, portanto, a aritmética seria feita no tipo de 33 bits.)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 ...)-m32
quanto-m64
com 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 é exibidopushl $3
epushl $-2
antes da chamadaprintf
, 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.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 tipolong long int
para evitar avisos ao compilar com clang.Aqui está a fonte:
Aqui está a saída do programa compilada com clang de 64 bits:
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:
O que é consistente com cada largura tendo um tipo diferente.
A expressão
E1 << E2
tem o tipo do operando esquerdo promovido, portanto, qualquer largura menor queINT_WIDTH
é promovida paraint
via promoção inteira e qualquer largura maior queINT_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 + E2
e outros operadores aritméticos seE1
ouE2
são campos de bit, com uma largura maior do que a daint
. 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.
fonte
int
puder representar todos os valores do tipo original (conforme restrito pela largura, para um campo de bits), o valor será convertido em umint
; caso contrário, ele é convertido em umunsigned 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 umint
.int
,unsigned int
e_Bool
.int
e não devem ser fixos 32.uint64_t
campos 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),int
isso não significa que eles estão agrupados em 32 bitsunsigned int
, mas é isso que uma leitura literal de 6.3. 1.1 diz.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:
A saída no modo C (sinalizadores
-xc -O2 -m32
)O problema é a última instrução
and edx, 1048575
que corta os 12 bits mais significativos.A saída no modo C ++ é idêntica, exceto pela última instrução:
A saída no modo de 64 bits é muito mais simples e correta, mas diferente para os compiladores C e C ++:
Você deve registrar um relatório de bug no rastreador de erros do gcc.
fonte