web-dev-qa-db-fra.com

Cast efficace non signé sur signé évitant le comportement défini par l'implémentation

Je veux définir une fonction qui prend un unsigned int Comme argument et renvoie un int modulo congru UINT_MAX + 1 à l'argument.

Une première tentative pourrait ressembler à ceci:

int unsigned_to_signed(unsigned n)
{
    return static_cast<int>(n);
}

Mais, comme le sait tout juriste spécialisé dans les langues, la conversion de non signé en signé pour des valeurs supérieures à INT_MAX est définie par l'implémentation.

Je veux implémenter ceci de telle sorte que (a) il ne repose que sur le comportement mandaté par la spécification; et (b) il se compile en un no-op sur n'importe quelle machine moderne et optimise le compilateur.

Quant aux machines bizarres ... S'il n'y a pas d'intul modulo congru signé UINT_MAX + 1 à l'int entier non signé, disons que je veux lever une exception. S'il y en a plusieurs (je ne suis pas sûr que ce soit possible), disons que je veux le plus grand.

OK, deuxième tentative:

int unsigned_to_signed(unsigned n)
{
    int int_n = static_cast<int>(n);

    if (n == static_cast<unsigned>(int_n))
        return int_n;

    // else do something long and complicated
}

Je ne me soucie pas beaucoup de l'efficacité lorsque je ne suis pas sur un système à deux compléments typique, car à mon humble avis, c'est peu probable. Et si mon code devient un goulot d'étranglement sur les systèmes omniprésents de signe-amplitude de 2050, eh bien, je parie que quelqu'un peut le comprendre et l'optimiser ensuite.

Maintenant, cette deuxième tentative est assez proche de ce que je veux. Bien que la conversion en int soit définie par l'implémentation pour certaines entrées, la conversion en unsigned est garantie par la norme pour conserver la valeur modulo UINT_MAX + 1. Le conditionnel vérifie donc exactement ce que je veux, et il ne se compilera en rien sur n'importe quel système que je suis susceptible de rencontrer.

Cependant ... je continue à transtyper vers int sans d'abord vérifier s'il invoquera un comportement défini par l'implémentation. Sur un système hypothétique en 2050, il pourrait faire qui sait quoi. Alors disons que je veux éviter ça.

Question: À quoi devrait ressembler ma "troisième tentative"?

Pour récapituler, je veux:

  • Diffuser d'un entier non signé vers un entier signé
  • Conserver la valeur mod UINT_MAX + 1
  • Invoquer uniquement le comportement obligatoire standard
  • Compiler en un no-op sur une machine typique à deux compléments avec un compilateur d'optimisation

[Mise à jour]

Permettez-moi de donner un exemple pour montrer pourquoi ce n'est pas une question banale.

Considérons une implémentation C++ hypothétique avec les propriétés suivantes:

  • sizeof(int) est égal à 4
  • sizeof(unsigned) est égal à 4
  • INT_MAX Vaut 32 767
  • INT_MIN Vaut -232 + 32768
  • UINT_MAX Vaut 232 - 1
  • L'arithmétique sur int est modulo 232 (dans la plage INT_MIN à INT_MAX)
  • std::numeric_limits<int>::is_modulo Est vrai
  • La conversion de unsigned n en int conserve la valeur de 0 <= n <= 32767 et donne zéro sinon

Sur cette implémentation hypothétique, il y a exactement une valeur int congruente (mod UINT_MAX + 1) à chaque valeur unsigned. Ma question serait donc bien définie.

Je prétends que cette implémentation hypothétique de C++ est entièrement conforme aux spécifications C++ 98, C++ 03 et C++ 11. J'avoue que je n'ai pas mémorisé chaque mot de chacun d'eux ... Mais je crois que j'ai lu attentivement les sections pertinentes. Donc, si vous voulez que j'accepte votre réponse, vous devez (a) citer une spécification qui exclut cette implémentation hypothétique ou (b) la gérer correctement.

En effet, une réponse correcte doit gérer chaque implémentation hypothétique autorisée par la norme. C'est ce que signifie "invoquer uniquement un comportement standardisé", par définition.

Soit dit en passant, std::numeric_limits<int>::is_modulo Est totalement inutile ici pour plusieurs raisons. D'une part, il peut être true même si les transtypages non signés en signés ne fonctionnent pas pour les grandes valeurs non signées. Pour un autre, il peut être true même sur des systèmes à complément à un ou à amplitude de signe, si l'arithmétique est simplement modulo sur toute la plage entière. Etc. Si votre réponse dépend de is_modulo, C'est faux.

[Mise à jour 2]

réponse de hvd m'a appris quelque chose: Mon implémentation hypothétique de C++ pour les entiers est pas permise par le C. moderne Les normes C99 et C11 sont très spécifiques sur la représentation des entiers signés; en effet, ils n'autorisent que la complémentarité à deux, la complémentarité à un et la magnitude des signes (section 6.2.6.2, paragraphe 2);).

Mais C++ n'est pas C. Comme il s'avère, ce fait est au cœur même de ma question.

La norme C++ 98 d'origine était basée sur le C89 beaucoup plus ancien, qui dit (section 3.1.2.5):

Pour chacun des types d'entiers signés, il existe un type d'entier non signé correspondant (mais différent) (désigné par le mot-clé unsigned) qui utilise la même quantité de stockage (y compris les informations de signe) et a les mêmes exigences d'alignement. La plage de valeurs non négatives d'un type entier signé est une sous-gamme du type entier non signé correspondant, et la représentation de la même valeur dans chaque type est la même.

C89 ne dit rien sur le fait de n'avoir qu'un seul bit de signe ou de n'autoriser que deux-complément/un-complément/une amplitude de signe.

La norme C++ 98 a adopté ce langage presque mot pour mot (section 3.9.1 paragraphe (3)):

Pour chacun des types d'entiers signés, il existe un correspondant (mais différent) type d'entier non signé: "unsigned char", "unsigned short int", "unsigned int "Et" unsigned long int ", Chacun occupant la même quantité de mémoire et ayant les mêmes exigences d'alignement (3.9) que le type entier signé correspondant; c'est-à-dire que chaque type entier signé a la même représentation d'objet que son type entier non signé correspondant. La plage de valeurs non négatives d'un type entier signé est une sous-gamme du type entier non signé correspondant, et la représentation de la valeur de chaque type correspondant signé/non signé doit être la même.

La norme C++ 03 utilise un langage essentiellement identique, tout comme C++ 11.

Aucune spécification C++ standard ne limite ses représentations entières signées à une spécification C, pour autant que je sache. Et rien n'impose un bit de signe unique ou quelque chose du genre. Tout ce qu'il dit est que non négatif les entiers signés doivent être une sous-gamme des unsigned correspondants.

Donc, encore une fois, je prétends que INT_MAX = 32767 avec INT_MIN = -232+32768 est autorisé. Si votre réponse suppose le contraire, elle est incorrecte sauf si vous citez une norme C++ qui me prouve le contraire.

82
Nemo

Extension de la réponse de user71404:

_int f(unsigned x)
{
    if (x <= INT_MAX)
        return static_cast<int>(x);

    if (x >= INT_MIN)
        return static_cast<int>(x - INT_MIN) + INT_MIN;

    throw x; // Or whatever else you like
}
_

Si _x >= INT_MIN_ (gardez à l'esprit les règles de promotion, _INT_MIN_ est converti en unsigned), puis _x - INT_MIN <= INT_MAX_, donc cela n'aura aucun débordement.

Si ce n'est pas évident, jetez un œil à la déclaration "Si _x >= -4u_, puis _x + 4 <= 3_.", Et gardez à l'esprit que _INT_MAX_ sera au moins égal à la valeur mathématique de - INT_MIN - 1.

Sur les systèmes les plus courants, où !(x <= INT_MAX) implique _x >= INT_MIN_, l'optimiseur devrait être en mesure (et sur mon système, est capable) de supprimer la deuxième vérification, de déterminer que les deux return Les instructions peuvent être compilées dans le même code et supprimer également la première vérification. Liste d'assemblages générée:

___Z1fj:
LFB6:
    .cfi_startproc
    movl    4(%esp), %eax
    ret
    .cfi_endproc
_

La mise en œuvre hypothétique dans votre question:

  • INT_MAX est égal à 32767
  • INT_MIN est égal à -232 + 32768

n'est pas possible, ne nécessite donc pas de considération particulière. _INT_MIN_ sera égal à _-INT_MAX_, ou à _-INT_MAX - 1_. Cela découle de la représentation par C des types entiers (6.2.6.2), qui requiert que n bits soient des bits de valeur, un bit soit un bit de signe, et n'autorise qu'une seule représentation d'interruption (sans inclure les représentations non valides car de bits de remplissage), à ​​savoir celui qui représenterait autrement un zéro négatif/_-INT_MAX - 1_. C++ n'autorise aucune représentation entière au-delà de ce que C permet.

Mettre à jour: Le compilateur de Microsoft ne remarque apparemment pas que _x > 10_ et _x >= 11_ testent la même chose. Il génère uniquement le code souhaité si _x >= INT_MIN_ est remplacé par _x > INT_MIN - 1u_, qu'il peut détecter comme la négation de _x <= INT_MAX_ (sur cette plate-forme).

[Mise à jour du questionneur (Nemo), développant notre discussion ci-dessous]

Je crois maintenant que cette réponse fonctionne dans tous les cas, mais pour des raisons compliquées. Je suis susceptible d'accorder la prime à cette solution, mais je veux capturer tous les détails sanglants au cas où quelqu'un s'en soucierait.

Commençons par C++ 11, section 18.3.3:

Le tableau 31 décrit l'en-tête _<climits>_.

...

Le contenu est le même que l'en-tête de bibliothèque C standard _<limits.h>_.

Ici, "Standard C" signifie C99, dont la spécification limite sévèrement la représentation des entiers signés. Ils sont comme des entiers non signés, mais avec un bit dédié au "signe" et zéro ou plusieurs bits dédiés au "remplissage". Les bits de remplissage ne contribuent pas à la valeur de l'entier et le bit de signe ne contribue qu'en tant que complément à deux, complément à un ou amplitude de signe.

Puisque C++ 11 hérite des macros _<climits>_ de C99, INT_MIN est soit -INT_MAX ou -INT_MAX-1, et le code de hvd est garanti pour fonctionner. (Notez que, en raison du remplissage, INT_MAX pourrait être beaucoup moins que UINT_MAX/2 ... Mais grâce à la façon dont les transtypages signés-> non signés fonctionnent, cette réponse gère cela très bien.)

C++ 03/C++ 98 est plus délicat. Il utilise le même libellé pour hériter _<climits>_ de "Standard C", mais maintenant "Standard C" signifie C89/C90.

Tous ces éléments - C++ 98, C++ 03, C89/C90 - ont le libellé que je donne dans ma question, mais incluent également ceci (C++ 03 section 3.9.1 paragraphe 7):

Les représentations des types intégraux doivent définir des valeurs en utilisant un système de numération binaire pur. (44) [ Exemple : la présente Norme internationale autorise le complément à 2, le complément à 1 et des représentations de magnitude signées pour les types intégraux.]

La note de bas de page (44) définit le "système de numération binaire pur":

Une représentation positionnelle pour les entiers qui utilise les chiffres binaires 0 et 1, dans laquelle les valeurs représentées par les bits successifs sont additives, commencent par 1 et sont multipliées par la puissance intégrale successive de 2, sauf peut-être pour le bit avec la position la plus élevée.

Ce qui est intéressant dans cette formulation, c'est qu'elle se contredit, car la définition de "système de numération binaire pur" ne permet pas une représentation signe/grandeur! Cela permet au bit haut d'avoir, disons, la valeur -2n-1 (complément à deux) ou - (2n-1-1) (compléments). Mais il n'y a pas de valeur pour le bit haut qui se traduit par signe/amplitude.

Quoi qu'il en soit, ma "mise en œuvre hypothétique" ne peut pas être qualifiée de "pur binaire" selon cette définition, elle est donc exclue.

Cependant, le fait que le bit haut soit spécial signifie que nous pouvons imaginer qu'il contribue à n'importe quelle valeur: une petite valeur positive, une énorme valeur positive, une petite valeur négative ou une énorme valeur négative. (Si le bit de signe peut contribuer - (2n-1-1), pourquoi pas - (2n-1-2)? etc.)

Imaginons donc une représentation d'entier signé qui attribue une valeur farfelue au bit "signe".

Une petite valeur positive pour le bit de signe entraînerait une plage positive pour int (peut-être aussi grande que unsigned), et le code de hvd le gère très bien.

Une énorme valeur positive pour le bit de signe aurait pour résultat que int aura un maximum supérieur à unsigned, ce qui est interdit.

Une énorme valeur négative pour le bit de signe se traduirait par int représentant une plage de valeurs non contiguës, et d'autres termes dans les règles de spécification qui sortent.

Enfin, que diriez-vous d'un bit de signe qui contribue une petite quantité négative? Pourrions-nous avoir un 1 dans le "bit de signe" qui contribue, disons, -37 à la valeur de l'int? Donc, INT_MAX serait (disons) 231-1 et INT_MIN seraient -37?

Il en résulterait que certains nombres ont deux représentations ... Mais le complément à un donne deux représentations à zéro, ce qui est autorisé selon l '"Exemple". Nulle part la spécification ne dit que zéro est le seul entier qui pourrait avoir deux représentations. Je pense donc que cette nouvelle hypothèse est autorisée par la spécification.

En effet, toute valeur négative de -1 à _-INT_MAX-1_ semble être autorisée en tant que valeur pour le "bit de signe", mais rien de plus petit (de peur que la plage ne soit non contiguë). En d'autres termes, _INT_MIN_ peut être compris entre _-INT_MAX-1_ et -1.

Maintenant, devinez quoi? Pour que la deuxième distribution dans le code de hvd évite un comportement défini par l'implémentation, nous avons juste besoin de x - (unsigned)INT_MIN inférieur ou égal à _INT_MAX_. Nous venons de montrer que _INT_MIN_ est au moins _-INT_MAX-1_. De toute évidence, x est au plus _UINT_MAX_. La conversion d'un nombre négatif en non signé équivaut à l'ajout de _UINT_MAX+1_. Mets le tout ensemble:

_x - (unsigned)INT_MIN <= INT_MAX
_

si et seulement si

_UINT_MAX - (INT_MIN + UINT_MAX + 1) <= INT_MAX
-INT_MIN-1 <= INT_MAX
-INT_MIN <= INT_MAX+1
INT_MIN >= -INT_MAX-1
_

C'est ce que nous venons de montrer, donc même dans ce cas pervers, le code fonctionne réellement.

Cela épuise toutes les possibilités, mettant ainsi fin à cet exercice extrêmement académique.

Bottom line: Il existe un comportement sérieusement sous-spécifié pour les entiers signés dans C89/C90 hérités par C++ 98/C++ 03. Il est corrigé dans C99 et C++ 11 hérite indirectement du correctif en incorporant _<limits.h>_ à partir de C99. Mais même C++ 11 conserve le libellé auto-contradictoire de "représentation binaire pure" ...

66
user743382

Ce code ne dépend que du comportement, mandaté par la spécification, donc l'exigence (a) est facilement satisfaite:

int unsigned_to_signed(unsigned n)
{
  int result = INT_MAX;

  if (n > INT_MAX && n < INT_MIN)
    throw runtime_error("no signed int for this number");

  for (unsigned i = INT_MAX; i != n; --i)
    --result;

  return result;
}

Ce n'est pas si facile avec l'exigence (b). Cela se compile en un no-op avec gcc 4.6.3 (-Os, -O2, -O3) et avec clang 3.0 (-Os, -O, -O2, -O3). Intel 12.1.0 refuse d'optimiser cela. Et je n'ai aucune information sur Visual C.

17
Evgeny Kluev

Vous pouvez explicitement dire au compilateur ce que vous voulez faire:

int unsigned_to_signed(unsigned n) {
  if (n > INT_MAX) {
    if (n <= UINT_MAX + INT_MIN) {
      throw "no result";
    }
    return static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1);
  } else {
    return static_cast<int>(n);
  }
}

Compile avec gcc 4.7.2 pour x86_64-linux (g++ -O -S test.cpp) à

_Z18unsigned_to_signedj:
    movl    %edi, %eax
    ret
3
user71404

Mon argent est sur l'utilisation de memcpy. Tout compilateur décent sait l'optimiser:

#include <stdio.h>
#include <memory.h>
#include <limits.h>

static inline int unsigned_to_signed(unsigned n)
{
    int result;
    memcpy( &result, &n, sizeof(result));
    return result;
}

int main(int argc, const char * argv[])
{
    unsigned int x = UINT_MAX - 1;
    int xx = unsigned_to_signed(x);
    return xx;
}

Pour moi (Xcode 8.3.2, Apple LLVM 8.1, -O3), cela produit:

_main:                                  ## @main
Lfunc_begin0:
    .loc    1 21 0                  ## /Users/Someone/main.c:21:0
    .cfi_startproc
## BB#0:
    pushq    %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    ##DEBUG_VALUE: main:argc <- %EDI
    ##DEBUG_VALUE: main:argv <- %RSI
Ltmp3:
    ##DEBUG_VALUE: main:x <- 2147483646
    ##DEBUG_VALUE: main:xx <- 2147483646
    .loc    1 24 5 prologue_end     ## /Users/Someone/main.c:24:5
    movl    $-2, %eax
    popq    %rbp
    retq
Ltmp4:
Lfunc_end0:
    .cfi_endproc
2
Someone

Si x est notre entrée ...

Si x > INT_MAX, Nous voulons trouver une constante k telle que 0 <x - k*INT_MAX <INT_MAX.

C'est facile - unsigned int k = x / INT_MAX;. Ensuite, laissez unsigned int x2 = x - k*INT_MAX;

Nous pouvons maintenant convertir x2 En int en toute sécurité. Soit int x3 = static_cast<int>(x2);

Nous voulons maintenant soustraire quelque chose comme UINT_MAX - k * INT_MAX + 1 De x3, Si k > 0.

Maintenant, sur un système de complément 2s, tant que x > INT_MAX, Cela revient à:

unsigned int k = x / INT_MAX;
x -= k*INT_MAX;
int r = int(x);
r += k*INT_MAX;
r -= UINT_MAX+1;

Notez que UINT_MAX+1 Est zéro en C++ garanti, la conversion en int était un noop, et nous avons soustrait k*INT_MAX Puis l'avons rajouté sur "la même valeur". Un optimiseur acceptable devrait donc être en mesure d'effacer toutes ces tromperies!

Cela laisse le problème de x > INT_MAX Ou non. Eh bien, nous créons 2 branches, une avec x > INT_MAX Et une sans. Celui qui n'en a pas fait un cast étroit, que le compilateur optimise en noop. Celui avec ... fait un noop une fois l'optimiseur terminé. L'optimiseur intelligent réalise les deux branches sur la même chose et supprime la branche.

Problèmes: si UINT_MAX Est vraiment grand par rapport à INT_MAX, Ce qui précède peut ne pas fonctionner. Je suppose que k*INT_MAX <= UINT_MAX+1 Implicitement.

Nous pourrions probablement attaquer cela avec quelques énumérations comme:

enum { divisor = UINT_MAX/INT_MAX, remainder = UINT_MAX-divisor*INT_MAX };

qui fonctionnent à 2 et 1 sur un système de complément 2s je crois (sommes-nous garantis pour que les mathématiques fonctionnent? C'est délicat ...), et fais de la logique basée sur celles-ci qui optimisent facilement loin sur les systèmes de complément non-2 ...

Cela ouvre également le cas d'exception. Cela n'est possible que si UINT_MAX est beaucoup plus grand que (INT_MIN-INT_MAX), vous pouvez donc mettre votre code d'exception dans un bloc if en posant exactement cette question, et cela ne vous ralentira pas sur un système traditionnel.

Je ne sais pas exactement comment construire ces constantes au moment de la compilation pour gérer correctement cela.

2

Je pense que le type int est au moins deux octets, donc INT_MIN et INT_MAX peuvent changer sur différentes plates-formes.

types fondamentaux

en-tête ≤climits≥

1
user679937