Modifier 2 :
Je déboguais un échec de test étrange lorsqu'une fonction résidant précédemment dans un fichier source C ++ mais déplacée dans un fichier C textuellement, a commencé à renvoyer des résultats incorrects. Le MVE ci-dessous permet de reproduire le problème avec GCC. Cependant, quand j'ai, sur un coup de tête, compilé l'exemple avec Clang (et plus tard avec VS), j'ai obtenu un résultat différent! Je ne peux pas comprendre si cela doit être traité comme un bogue dans l'un des compilateurs, ou comme une manifestation d'un résultat indéfini autorisé par la norme C ou C ++. Étrangement, aucun des compilateurs ne m'a donné d'avertissement sur l'expression.
Le coupable est cette expression:
ctl.b.p52 << 12;
Ici, p52
est tapé comme uint64_t
; il fait également partie d'un syndicat (voir control_t
ci - dessous). L'opération de décalage ne perd aucune donnée car le résultat tient toujours en 64 bits. Cependant, GCC décide alors de tronquer le résultat à 52 bits si j'utilise le compilateur C ! Avec le compilateur C ++, les 64 bits de résultat sont conservés.
Pour illustrer cela, l'exemple de programme ci-dessous compile deux fonctions avec des corps identiques, puis compare leurs résultats. c_behavior()
est placé dans un fichier source C et cpp_behavior()
dans un fichier C ++, et main()
fait la comparaison.
Référentiel avec l'exemple de code: https://github.com/grigory-rechistov/c-cpp-bitfields
L'en-tête common.h définit une union de champs binaires larges de 64 bits et d'entier et déclare deux fonctions:
#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
Les fonctions ont des corps identiques, sauf que l'une est traitée en C et l'autre en 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;
}
principal 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 montre la différence entre les résultats qu'ils renvoient:
$ gcc -Wpedantic main.c c-part.c cpp-part.cpp
$ ./a.exe
OMG C != C++
Cependant, avec Clang C et C ++ se comportent de manière identique et comme prévu:
$ clang -Wpedantic main.c c-part.c cpp-part.cpp
$ ./a.exe
C == C++
Avec Visual Studio, j'obtiens le même résultat qu'avec 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++
J'ai essayé les exemples sous Windows, même si le problème d'origine avec GCC a été découvert sous Linux.
la source
<<
comme nécessitant la troncature.main.c
et provoque probablement un comportement indéfini de plusieurs manières. OMI, il serait plus clair de publier un MRE à fichier unique qui produit une sortie différente lors de la compilation avec chaque compilateur. Parce que l'interopérabilité C-C ++ n'est pas bien spécifiée par la norme. Notez également que l'alias de l'union provoque UB en C ++.Réponses:
C et C ++ traitent différemment les types de membres du champ binaire.
C 2018 6.7.2.1 10 dit:
Observez que ce n'est pas spécifique au type - c'est un type entier - et cela ne dit pas que le type est le type qui a été utilisé pour déclarer le champ de bits, comme
uint64_t a : 1;
indiqué dans la question. Cela laisse apparemment la possibilité à l'implémentation de choisir le type.C ++ 2017 draft n4659 12.2.4 [class.bit] 1 dit, d'une déclaration de champ de bits:
Cela implique que, dans une déclaration telle que
uint64_t a : 1;
, le: 1
ne fait pas partie du type du membre de classea
, donc le type est comme s'il l'étaituint64_t a;
, et donc le type dea
estuint64_t
.Il semble donc que GCC traite un champ binaire en C comme un type entier 32 bits ou plus étroit s'il convient et un champ binaire en C ++ comme son type déclaré, et cela ne semble pas enfreindre les normes.
la source
E1
dans ce cas, il s'agit d'un champ binaire de 52 bits.uint64_t a : 33
ensemble à 2 ^ 33−1 dans une structures
, alors, dans une implémentation C avec 32 bitsint
,s.a+s.a
devrait donner 2 ^ 33−2 en raison de l'habillage, mais Clang produit 2 ^ 34− 2; il le traite apparemment commeuint64_t
.s.a+s.a
, les conversions arithmétiques habituelles ne changeraient pas le type des.a
, car elles sont plus larges queunsigned int
, donc l'arithmétique se ferait dans le type 33 bits.)uint64_t
. S'il s'agit d'une compilation 64 bits, cela semble rendre Clang cohérent avec la façon dont GCC traite les compilations 64 bits en ne tronquant pas. Clang traite-t-il les compilations 32 et 64 bits différemment? (Et il semble que je viens d'apprendre une autre raison d'éviter les champs de bits ...)-m32
et-m64
, avec un avertissement que le type est une extension GCC. Avec Apple Clang 11.0, je n'ai pas de bibliothèques pour exécuter du code 32 bits, mais l'assembly généré montrepushl $3
etpushl $-2
avant d'appelerprintf
, donc je pense que c'est 2 ^ 34−2. Donc, Apple Clang ne diffère pas entre les cibles 32 bits et 64 bits, mais a changé au fil du temps.Andrew Henle a suggéré une interprétation stricte de la norme C: le type d'un champ binaire est un type entier signé ou non signé avec exactement la largeur spécifiée.
Voici un test qui supporte cette interprétation: utiliser le C1x
_Generic()
construction , j'essaie de déterminer le type de champs binaires de différentes largeurs. J'ai dû les définir avec le typelong long int
pour éviter les avertissements lors de la compilation avec clang.Voici la source:
Voici la sortie du programme compilée avec clang 64 bits:
Tous les champs binaires semblent avoir le type défini plutôt qu'un type spécifique pour la largeur définie.
Voici la sortie du programme compilée avec gcc 64 bits:
Ce qui est cohérent avec chaque largeur ayant un type différent.
L'expression
E1 << E2
a le type de l'opérande gauche promu, donc toute largeur inférieure àINT_WIDTH
est promueint
via une promotion entière et toute largeur supérieure àINT_WIDTH
est laissée seule. Le résultat de l'expression doit en effet être tronqué à la largeur du champ binaire si cette largeur est supérieure àINT_WIDTH
. Plus précisément, il doit être tronqué pour un type non signé et il peut être défini par l'implémentation pour les types signés.La même chose devrait se produire pour les
E1 + E2
autres opérateurs arithmétiques siE1
ouE2
sont des champs binaires avec une largeur supérieure à celle deint
. L'opérande avec la plus petite largeur est converti en type avec la plus grande largeur et le résultat a également le type type. Ce comportement très contre-intuitif provoquant de nombreux résultats inattendus, peut être la cause de la croyance répandue que les champs binaires sont faux et doivent être évités.De nombreux compilateurs ne semblent pas suivre cette interprétation de la norme C, et cette interprétation ne ressort pas clairement du libellé actuel. Il serait utile de clarifier la sémantique des opérations arithmétiques impliquant des opérandes de champ binaire dans une future version de la norme C.
la source
int
peut représenter toutes les valeurs du type d'origine (limité par la largeur, pour un champ de bits), la valeur est convertie en anint
; sinon, il est converti en anunsigned int
. Celles-ci sont appelées promotions entières. - §6.3.1.8 , §6.7.2.1 ), ne couvre pas le cas où la largeur d'un champ binaire est plus large que anint
.int
,unsigned int
et_Bool
.int
et ne pas être un 32 fixe.uint64_t
champs binaires, la norme n'a rien à dire à leur sujet - elle devrait être couverte par la documentation de l'implémentation des parties du comportement définies par l'implémentation. de champs binaires. En particulier, le fait que les 52 bits du champ binaire ne tiennent pas dans un (32 bits)int
ne devrait pas signifier qu'ils sont compressés en 32 bitsunsigned int
, mais c'est ce qu'une lecture littérale de 6,3. 1.1 dit.Le problème semble être spécifique au générateur de code 32 bits de gcc en mode C:
Vous pouvez comparer le code assembleur à l'aide de Godbolt's Compiler Explorer
Voici le code source de ce test:
La sortie en mode C (drapeaux
-xc -O2 -m32
)Le problème est la dernière instruction
and edx, 1048575
qui coupe les 12 bits les plus significatifs.La sortie en mode C ++ est identique à l'exception de la dernière instruction:
La sortie en mode 64 bits est beaucoup plus simple et correcte, mais différente pour les compilateurs C et C ++:
Vous devez déposer un rapport de bogue sur le suivi des bogues gcc.
la source