Troncature incohérente des expressions entières de champ de bits non signées entre C ++ et C dans différents compilateurs

10

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, p52est tapé comme uint64_t; il fait également partie d'un syndicat (voir control_tci - 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.

Grigory Rechistov
la source
1
les champs binaires sont notoirement faux pour les grandes largeurs. Je suis tombé sur des problèmes similaires dans cette question: stackoverflow.com/questions/58846584/…
chqrlie
@chqrlie J'ai lu l' opérateur C<< comme nécessitant la troncature.
Andrew Henle
Veuillez publier un stackoverflow.com/help/minimal-reproducible-example . Le code actuel n'a pas main.cet 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 ++.
MM
@MM C'est vrai, ça a glissé quand j'ai posté la question. Je l'ai ajouté maintenant, et je pense aussi qu'avoir un petit dépôt avec lui pourrait aussi être une idée
Grigory Rechistov
@MM "IMO, il serait plus clair de publier un MRE à fichier unique qui produit une sortie différente lors de la compilation avec chaque compilateur." Je n'y ai pas pensé car je transformais mon code de production en quelque chose de plus petit, mais il devrait être possible de reformuler le reproducteur en un seul fichier.
Grigory Rechistov

Réponses:

6

C et C ++ traitent différemment les types de membres du champ binaire.

C 2018 6.7.2.1 10 dit:

Un champ de bits est interprété comme ayant un type entier signé ou non signé composé du nombre spécifié de bits…

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:

… L'attribut champ de bits ne fait pas partie du type du membre de classe…

Cela implique que, dans une déclaration telle que uint64_t a : 1;, le : 1ne fait pas partie du type du membre de classe a, donc le type est comme s'il l'était uint64_t a;, et donc le type de aest uint64_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.

Eric Postpischil
la source
J'ai lu la troncature en C comme obligatoire par 6.5.7 4 (le libellé C18 est similaire): "Le résultat de E1 << E2 est des positions de bits E2 décalées vers la gauche E1; les bits vides sont remplis de zéros. Si E1 a un type non signé , la valeur du résultat est E1 x 2E2, modulo réduit de plus que la valeur maximale représentable dans le type de résultat. " E1dans ce cas, il s'agit d'un champ binaire de 52 bits.
Andrew Henle
@AndrewHenle: Je vois ce que vous dites. Le type d'un champ binaire à n bits est « entier à n bits» (en négligeant la signature pour l'instant). Je l'interprétais comme le type d'un champ binaire à n bits est un type entier, que l'implémentation choisit. Sur la seule base du libellé du 6.7.2.1 10, je suis favorable à votre interprétation. Mais un problème avec cela est que, étant donné un uint64_t a : 33ensemble à 2 ^ 33−1 dans une structure s, alors, dans une implémentation C avec 32 bits int, s.a+s.adevrait donner 2 ^ 33−2 en raison de l'habillage, mais Clang produit 2 ^ 34− 2; il le traite apparemment comme uint64_t.
Eric Postpischil
@AndrewHenle: (Plus sur le raisonnement: dans s.a+s.a, les conversions arithmétiques habituelles ne changeraient pas le type de s.a, car elles sont plus larges que unsigned int, donc l'arithmétique se ferait dans le type 33 bits.)
Eric Postpischil
mais Clang produit 2 ^ 34−2; il le traite apparemment comme 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 ...)
Andrew Henle
@AndrewHenle: Eh bien, l'ancien Apple Clang 1.7 produit 2 ^ 32−2 (pas 2 ^ 33−2; il a perdu un peu!) À la fois avec -m32et -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é montre pushl $3et pushl $-2avant d'appeler printf, 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.
Eric Postpischil
4

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 type long long intpour éviter les avertissements lors de la compilation avec clang.

Voici la source:

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

Voici la sortie du programme compilée avec clang 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

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:

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

Ce qui est cohérent avec chaque largeur ayant un type différent.

L'expression E1 << E2a le type de l'opérande gauche promu, donc toute largeur inférieure à INT_WIDTHest promue intvia une promotion entière et toute largeur supérieure à INT_WIDTHest 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 + E2autres opérateurs arithmétiques si E1ou E2sont des champs binaires avec une largeur supérieure à celle de int. 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.

chqrlie
la source
1
Je pense que le terme clé est «promotions entières». La discussion des champs de bits avec des promotions entières (C11 §6.3.1.1 - Si an intpeut représenter toutes les valeurs du type d'origine (limité par la largeur, pour un champ de bits), la valeur est convertie en an int; sinon, il est converti en an unsigned 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 an int.
Jonathan Leffler
1
Cela n'aide pas que la norme laisse indéfini (au mieux défini par l'implémentation) quels types sont autorisés pour les champs binaires autres que int, unsigned intet _Bool.
Jonathan Leffler
1
"toute largeur inférieure à 32", "toute largeur supérieure à 32" et "si cette largeur est supérieure à 32" devraient probablement refléter le nombre de bits en clair intet ne pas être un 32 fixe.
Ben Voigt
1
Je suis d'accord qu'il y a un problème (de surveillance) dans la norme C. Il pourrait être possible de faire valoir que, puisque la norme ne sanctionne pas l'utilisation des uint64_tchamps 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) intne devrait pas signifier qu'ils sont compressés en 32 bits unsigned int, mais c'est ce qu'une lecture littérale de 6,3. 1.1 dit.
Jonathan Leffler
1
De plus, si C ++ a résolu explicitement les problèmes de `` gros champ de bits '', alors C devrait suivre cet exemple aussi étroitement que possible - à moins qu'il y ait quelque chose de intrinsèquement spécifique à C ++ à propos de cette résolution (ce qui n'est pas probable).
Jonathan Leffler
2

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:

#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 sortie en mode C (drapeaux -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

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:

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 sortie en mode 64 bits est beaucoup plus simple et correcte, mais différente pour les compilateurs C et C ++:

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

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

Vous devez déposer un rapport de bogue sur le suivi des bogues gcc.

chqrlie
la source
Mes expériences ne concernaient que des cibles 64 bits, mais votre cas 32 bits est encore plus bisarre. Je suppose qu'un rapport de bogue est dû. Tout d'abord, je dois le revérifier sur une dernière version de GCC à ma disposition.
Grigory Rechistov
1
@GrigoryRechistov Compte tenu du libellé de la norme C , le bogue peut très bien être la cible 64 bits ne parvenant pas à tronquer le résultat à 52 bits. Personnellement, je le verrais de cette façon.
Andrew Henle