28 votes

Le "&" par bit avec un opérande signé ou non signé

J'ai été confronté à un scénario intéressant dans lequel j'ai obtenu des résultats différents selon le bon type d'opérande, et je n'arrive pas vraiment à en comprendre la raison.

Voici le code minimal :

#include <iostream>
#include <cstdint>

int main()
{
    uint16_t check = 0x8123U;

    uint64_t new_check = (check & 0xFFFF) << 16;

    std::cout << std::hex << new_check << std::endl;

    new_check = (check & 0xFFFFU) << 16;

    std::cout << std::hex << new_check << std::endl;

    return 0;
}

J'ai compilé ce code avec g++ (gcc version 4.5.2) sur Linux 64bit : g++ -std=c++0x -Wall exemple.cpp -o exemple

Le résultat était :

ffffff81230000

81230000

Je ne comprends pas vraiment la raison de la sortie dans le premier cas.

Pourquoi, à un moment donné, l'un des résultats du calcul temporel serait-il promu au rang de signé 64bit valeur ( int64_t ) entraînant l'extension du signe ?

J'accepterais un résultat de "0" dans les deux cas si une valeur de 16 bits était d'abord décalée de 16 bits vers la gauche, puis promue à une valeur de 64 bits. J'accepte également le second résultat si le compilateur promeut d'abord la valeur de 16 bits à 64 bits. check a uint64_t et effectue ensuite les autres opérations.

Mais comment se fait-il que & avec 0xFFFF ( int32_t ) contre 0xFFFFU ( uint32_t ) donnerait lieu à ces deux résultats différents ?

21voto

Serge Ballesta Points 12850

C'est en effet un cas particulier intéressant. Il ne se produit ici que parce que vous utilisez uint16_t pour le type non signé lorsque l'architecture utilise 32 bits pour le type non signé. ìnt

Voici un extrait de Clause 5 Expressions du projet n4296 pour C++14 (c'est moi qui souligne) :

10 De nombreux opérateurs binaires qui attendent des opérandes de type arithmétique ou énumération provoquent des conversions .... Ce schéma est appelé les conversions arithmétiques habituelles, qui sont définies comme suit :
...
(10.5.3) - Sinon, si l'opérande qui a Le type d'entier non signé a un rang supérieur ou égal à celui de l'élément rang du type de l'autre opérande l'opérande de type entier signé est converti en un type d'opérande de type entier non signé. le type de l'opérande avec un type d'entier non signé.
(10.5.4) - Sinon, si le type de l'opérande à type d'entier signé peut représenter toutes les valeurs de le type de l'opérande avec le type entier non signé l'opérande dont le type est un entier non signé est être converti en type d'opérande de type entier signé.

Vous êtes dans le cas de la 10.5.4 :

  • uint16_t n'est que de 16 bits alors que int est de 32
  • int peut représenter toutes les valeurs de uint16_t

Ainsi, le uint16_t check = 0x8123U est converti en l'opérande signé 0x8123 et le résultat de l'analyse binaire & est toujours 0x8123.

Mais le décalage (par bit, donc au niveau de la représentation) fait que le résultat est le non signé intermédiaire 0x81230000 qui, converti en un int, donne une valeur négative (techniquement, c'est défini par l'implémentation, mais cette conversion est un usage courant).

5.8 Opérateurs de décalage [expr.shift].
...
Sinon, si E1 a un type signé et une valeur non négative, et E1×2 E2 est représentable dans le type non signé correspondant du type de résultat, alors cette valeur, convertie en type de résultat, est la valeur résultante ;...

et

4.7 Conversions intégrales [conv.integral].
...
3 Si le type de destination est signé, la valeur est inchangée si elle peut être représentée dans le type de destination ; sinon, la valeur est définie par la mise en œuvre .

(attention, il s'agissait d'un véritable comportement indéfini en C++11...)

On obtient donc une conversion de l'int signé 0x81230000 en un uint64_t qui comme prévu donne 0xFFFFFFFF81230000, car

4.7 Conversions intégrales [conv.integral].
...
2 Si le type de la destination est non signé, la valeur résultante est le plus petit entier non signé congruent à l'entier source. source (modulo 2n où n est le nombre de bits utilisés pour représenter le type non signé).

TL/DR : Il n'y a pas de comportement indéfini ici, ce qui cause le résultat est la conversion de signed 32 bits int en unsigned 64 bits int. La seule partie qui est comportement indéfini est un décalage qui causerait un dépassement de signe, mais toutes les implémentations communes partagent celui-ci et c'est mise en œuvre définie dans la norme C++14.

Bien sûr, si vous forcez le second opérande à être non signé, tout est non signé et vous obtenez évidemment la bonne réponse. 0x81230000 résultat.

[Comme expliqué par MSalters, le résultat du décalage est seulement mise en œuvre définie depuis C++14, mais était en effet comportement indéfini en C++11. Le paragraphe sur l'opérateur shift dit :

...
Sinon, si E1 a un type signé et une valeur non négative, et E1×2 E2 est représentable dans le type de résultat alors c'est la valeur qui en résulte ; sinon, le comportement est indéfini .

10voto

MSalters Points 74024

La première chose à savoir est que les opérateurs binaires tels que a&b pour les types intégrés ne fonctionnent que si les deux côtés ont le même type. (Avec les types définis par l'utilisateur et les surcharges, tout est permis). Ceci peut être réalisé par des conversions implicites.

Dans votre cas, il y a bien une telle conversion, car il n'existe pas d'opérateur binaire. & qui prend un type plus petit que int . Les deux côtés sont convertis à au moins int la taille, mais quels types exacts ?

En l'occurrence, sur votre GCC int est bien de 32 bits. Ceci est important, car cela signifie que toutes les valeurs de uint16_t peut être représenté comme un int . Il n'y a pas de débordement.

D'où, check & 0xFFFF est un cas simple. Le côté droit est déjà un int le côté gauche promeut int donc le résultat est int(0x8123) . C'est tout à fait normal.

Maintenant, l'opération suivante est 0x8123 << 16 . Rappelez-vous, sur votre système int est de 32 bits, et INT_MAX est 0x7FFF'FFFF . En l'absence de débordement, 0x8123 << 16 serait 0x81230000 mais c'est clairement plus grand que INT_MAX donc il y a en fait un débordement.

Le dépassement des entiers signés en C++11 est un comportement indéfini . Littéralement n'importe quel résultat est correct, y compris purple ou pas de sortie du tout. Au moins, vous avez obtenu une valeur numérique, mais GCC est connu pour éliminer purement et simplement les chemins de code qui provoquent inévitablement un débordement.

[modifier] Les versions plus récentes de GCC supportent C++14, où cette forme particulière de débordement est devenu défini par l'implémentation - voir la réponse de Serge.

9voto

Rishikesh Raje Points 4234

Jetons un coup d'œil à

uint64_t new_check = (check & 0xFFFF) << 16;

Ici, 0xFFFF est une constante signée, donc (check & 0xFFFF) nous donne un entier signé selon les règles de la promotion des entiers.

Dans votre cas, avec le système 32-bit int le MSbit de ce nombre entier après le décalage vers la gauche est 1, et donc l'extension à 64-bit unsigned fera une extension de signe, remplissant les bits à gauche avec des 1. Interprété comme une représentation en complément à deux qui donne la même valeur négative.

Dans le second cas, 0xFFFFU est non signée, nous obtenons donc des entiers non signés et l'opérateur de décalage à gauche fonctionne comme prévu.

Si votre chaîne d'outils supporte __PRETTY_FUNCTION__ une fonctionnalité très pratique, vous pouvez rapidement déterminer comment le compilateur perçoit les types d'expression :

#include <iostream>
#include <cstdint>

template<typename T>
void typecheck(T const& t)
{
    std::cout << __PRETTY_FUNCTION__ << '\n';
    std::cout << t << '\n';
}
int main()
{
    uint16_t check = 0x8123U;

    typecheck(0xFFFF);
    typecheck(check & 0xFFFF);
    typecheck((check & 0xFFFF) << 16);

    typecheck(0xFFFFU);
    typecheck(check & 0xFFFFU);
    typecheck((check & 0xFFFFU) << 16);

    return 0;
}

Salida

void typecheck(const T &) [T = int]
65535
void typecheck(const T &) [T = int]
33059
void typecheck(const T &) [T = int]
-2128412672
void typecheck(const T &) [T = unsigned int]
65535
void typecheck(const T &) [T = unsigned int]
33059
void typecheck(const T &) [T = unsigned int]
2166554624

2voto

kfsone Points 7375

0xFFFF est un int signé. Ainsi, après le & nous avons une valeur signée de 32 bits :

#include <stdint.h>
#include <type_traits>

uint64_t foo(uint16_t a) {
  auto x = (a & 0xFFFF);
  static_assert(std::is_same<int32_t, decltype(x)>::value, "not an int32_t")
  static_assert(std::is_same<uint16_t, decltype(x)>::value, "not a uint16_t");
  return x;
}

http://ideone.com/tEQmbP

Vos 16 bits d'origine sont ensuite décalés vers la gauche, ce qui donne une valeur de 32 bits avec le bit de poids fort (0x80000000U), qui a donc une valeur négative. Pendant la conversion 64 bits, l'extension du signe se produit, remplissant les mots supérieurs avec des 1.

1voto

Groo Points 19453

C'est le résultat de la promotion des nombres entiers. Avant que le & se produit, si les opérandes sont "plus petits" qu'un int (pour cette architecture), le compilateur promouvra les deux opérandes en tant que int parce qu'ils entrent tous les deux dans une signed int :

Cela signifie que la première expression sera équivalente à (sur une architecture 32 bits) :

// check is uint16_t, but it fits into int32_t.
// the constant is signed, so it's sign-extended into an int
((int32_t)check & (int32_t)0xFFFFFFFF)

tandis que l'autre aura le deuxième opérande promu à :

// check is uint16_t, but it fits into int32_t.
// the constant is unsigned, so the upper 16 bits are zero
((int32_t)check & (int32_t)0x0000FFFFU)

Si vous lancez explicitement check à un unsigned int alors le résultat sera le même dans les deux cas ( unsigned * signed aura pour résultat unsigned ):

((uint32_t)check & 0xFFFF) << 16

sera égal à :

((uint32_t)check & 0xFFFFU) << 16

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X