95 votes

Détection des débordements signés en C/C++

À première vue, cette question peut sembler être une duplication de Comment détecter un dépassement d'entier ? Cependant, elle est en réalité très différente.

J'ai constaté que si la détection d'un dépassement de capacité d'un entier non signé est assez triviale, la détection d'un dépassement de capacité d'un entier non signé est plus difficile. signé Le débordement en C/C++ est en fait plus difficile que la plupart des gens ne le pensent.

La façon la plus évidente, mais aussi la plus naïve, de le faire serait quelque chose comme :

int add(int lhs, int rhs)
{
 int sum = lhs + rhs;
 if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) {
  /* an overflow has occurred */
  abort();
 }
 return sum; 
}

Le problème est que, selon la norme C, le dépassement des nombres entiers signés est comportement non défini. En d'autres termes, selon la norme, dès que vous provoquez ne serait-ce qu'un débordement signé, votre programme est tout aussi invalide que si vous déréférenciez un pointeur nul. Vous ne pouvez donc pas provoquer un comportement indéfini, puis essayer de détecter le débordement après coup, comme dans l'exemple de vérification post-condition ci-dessus.

Même si la vérification ci-dessus est susceptible de fonctionner sur de nombreux compilateurs, vous ne pouvez pas compter dessus. En effet, comme la norme C stipule que le débordement d'un entier signé n'est pas défini, certains compilateurs (comme GCC) vont optimiser le contrôle ci-dessus lorsque les drapeaux d'optimisation sont activés, car le compilateur suppose qu'un dépassement de capacité signé est impossible. Cela casse totalement la tentative de vérifier le débordement.

Donc, une autre façon possible de vérifier le débordement serait :

int add(int lhs, int rhs)
{
 if (lhs >= 0 && rhs >= 0) {
  if (INT_MAX - lhs <= rhs) {
   /* overflow has occurred */
   abort();
  }
 }
 else if (lhs < 0 && rhs < 0) {
  if (lhs <= INT_MIN - rhs) {
   /* overflow has occurred */
   abort();
  }
 }

 return lhs + rhs;
}

Cela semble plus prometteur, puisque nous n'ajoutons pas réellement les deux entiers ensemble avant de nous assurer à l'avance qu'une telle addition n'entraînera pas de dépassement de capacité. Ainsi, nous ne provoquons pas de comportement non défini.

Cependant, cette solution est malheureusement beaucoup moins efficace que la solution initiale, puisque vous devez effectuer une opération de soustraction juste pour tester si votre opération d'addition fonctionnera. Et même si vous ne vous souciez pas de cette (petite) perte de performance, je ne suis toujours pas convaincu que cette solution soit adéquate. L'expression lhs <= INT_MIN - rhs ressemble exactement au type d'expression que le compilateur pourrait optimiser, en pensant que le dépassement de capacité signé est impossible.

Y a-t-il une meilleure solution ? Quelque chose qui garantisse 1) de ne pas causer de comportement non défini, et 2) de ne pas donner au compilateur l'opportunité d'optimiser les vérifications de débordement ? Je pensais qu'il pourrait y avoir un moyen de le faire en convertissant les deux opérandes en non signés, et en effectuant des vérifications en utilisant votre propre arithmétique de complément à deux, mais je ne suis pas vraiment sûr de savoir comment faire.

1 votes

Plutôt que d'essayer de le détecter, n'est-il pas préférable d'écrire du code qui n'a pas de possibilité de débordement ?

0 votes

Le fait d'avoir la propriété "Comportement non défini" n'implique pas l'équivalence. En d'autres termes, le déréférencement d'un pointeur NULL n'est pas équivalent à un dépassement de capacité signé. UB signifie que la spécification ne précise pas le résultat requis pour être conforme à la spécification. On ne peut donc pas supposer que le système sera dans un état invalide et instable à cause du débordement signé, mais simplement que le résultat numérique ne sera pas prévisible.

10 votes

@ArunSaha : Il est vraiment difficile de prendre des calculs et de garantir qu'ils ne déborderont pas, et il est impossible de le prouver dans le cas général. La pratique habituelle consiste à utiliser un type d'entier aussi large que possible et à espérer.

44voto

Jens Gustedt Points 40410

Non, votre deuxième code n'est pas correct, mais vous êtes proche : si vous définissez

int half = INT_MAX/2;
int half1 = half + 1;

le résultat d'une addition est INT_MAX . ( INT_MAX est toujours un nombre impair). C'est donc une entrée valide. Mais dans votre routine, vous aurez INT_MAX - half == half1 et vous avorteriez. Un faux positif.

Cette erreur peut être réparée en mettant < au lieu de <= dans les deux contrôles.

Mais alors votre code n'est pas non plus optimal. Ce qui suit ferait l'affaire :

int add(int lhs, int rhs)
{
 if (lhs >= 0) {
  if (INT_MAX - lhs < rhs) {
   /* would overflow */
   abort();
  }
 }
 else {
  if (rhs < INT_MIN - lhs) {
   /* would overflow */
   abort();
  }
 }
 return lhs + rhs;
}

Pour voir que cela est valide, il faut ajouter symboliquement lhs des deux côtés des inégalités, et cela vous donne exactement les conditions arithmétiques que votre résultat est hors limites.

0 votes

+1 pour la meilleure réponse. Mineur : suggérer /* overflow will occurred */ pour souligner que le but est de détecter que le dépassement de capacité se serait produit si le code avait fait lhs + rhs sans faire réellement la somme.

0 votes

Une petite optimisation que vous pourriez faire, je suppose que cela dépendrait de votre matériel, je ne suis pas sûr de ce qui est le mieux, mais si vous utilisez un if et un else if avec (lhs > 0 && rhs > 0) et (lhs < 0 && rhs < 0) cela vous permettrait d'éviter de faire une soustraction dans les cas où les signes ne correspondent pas ou si l'une des valeurs est 0, mais cela nécessiterait 4 comparaisons dans ces cas et une comparaison supplémentaire dans le cas où les deux valeurs sont négatives. Lesquelles sont les plus rapides dans le matériel ? Les comparaisons ou les opérations arithmétiques telles que les soustractions ?

28voto

R.. Points 93718

Votre approche de la soustraction est correcte et bien définie. Un compilateur ne peut pas l'optimiser.

Une autre approche correcte, si vous disposez d'un type d'entier plus grand, consiste à effectuer l'arithmétique dans le type le plus grand, puis à vérifier que le résultat tient dans le type le plus petit lors de la reconversion.

int sum(int a, int b)
{
    long long c;
    assert(LLONG_MAX>INT_MAX);
    c = (long long)a + b;
    if (c < INT_MIN || c > INT_MAX) abort();
    return c;
}

Un bon compilateur devrait convertir toute l'addition et les if dans un int -et un seul saut conditionnel sur dépassement de capacité, sans jamais effectuer la plus grande addition.

Editar: Comme Stephen l'a fait remarquer, j'ai du mal à faire en sorte qu'un compilateur (pas si bon), gcc, génère un asm sain. Le code qu'il génère n'est pas terriblement lent, mais certainement sous-optimal. Si quelqu'un connaît des variantes de ce code qui permettront à gcc de faire ce qu'il faut, j'aimerais les voir.

1 votes

Pour ceux qui veulent l'utiliser, assurez-vous de regarder ma version éditée. Dans l'original j'ai stupidement omis la distribution à long long avant l'ajout.

0 votes

Alors, selon votre définition, gcc 4.4.4 n'est pas un bon compilateur. Je viens de l'essayer à -O2 y -O3 et j'ai obtenu le même code, 12 lignes d'assemblage (sans compter les commandes de l'assembleur) où l'addition réelle est effectuée par leaq (%rax,%rcx), %rcx o addl %eax, %ecx ; adcl %edx, %ebx si le -m32 est donné.

0 votes

De plus, gcc n'a pas supprimé le if du code généré.

16voto

JaredPar Points 333733

À mon avis, la manière la plus simple de traiter le code C++ avec un débordement sentencieux est d'utiliser SafeInt<T> . Il s'agit d'un modèle C++ multiplateforme hébergé sur code plex qui offre les garanties de sécurité que vous souhaitez ici.

Je trouve son utilisation très intuitive, car il fournit les mêmes modèles d'utilisation que les opérations numériques normales et exprime les flux supérieurs et inférieurs via des exceptions.

11voto

Rook Points 34698

Si vous utilisez l'assembleur en ligne, vous pouvez vérifier l'option drapeau de débordement . Une autre possibilité est d'utiliser un système de gestion de l'information. type de données safeint . Je vous recommande de lire ce document sur Sécurité des nombres entiers .

6 votes

+1 C'est une autre façon de dire "Si le C ne le définit pas, alors vous êtes contraint à un comportement spécifique à la plate-forme". Tant de choses qui sont facilement prises en charge en assemblage sont indéfinies en C, créant des montagnes à partir de taupinières au nom de la portabilité.

0 votes

Le débordement signé est parfaitement détectable et évitable en C. -1 pour avoir suggéré l'asm au lieu de suggérer le code C qui générera l'asm correct sur un bon compilateur.

0 votes

@R Je suis d'accord avec Rook.

1voto

supercat Points 25534

Pourquoi pas :

int sum(int n1, int n2)
{
  int result;
  if (n1 >= 0)
  {
    result = (n1 - INT_MAX)+n2; /* Can't overflow */
    if (result > 0) return INT_MAX; else return (result + INT_MAX);
  }
  else
  {
    result = (n1 - INT_MIN)+n2; /* Can't overflow */
    if (0 > result) return INT_MIN; else return (result + INT_MIN);
  }
}

Je pense que ça devrait marcher pour n'importe quel INT_MIN y INT_MAX (symétrique ou non) ; la fonction comme indiqué dans les clips, mais il devrait être évident comment obtenir d'autres comportements).

0 votes

+1 pour une approche alternative agréable qui est peut-être plus intuitive.

1 votes

Je pense que result = (n1 - INT_MAX)+n2; - pourrait déborder, si n1 est petit (disons 0) et n2 est négatif.

0 votes

@davmac : Hmm... peut-être qu'il faut distinguer trois cas : commencer par un pour (n1 ^ n2) < 0 ce qui, sur une machine à complément à deux, impliquerait que les valeurs sont de signe opposé et peuvent être additionnées directement. Si les valeurs ont le même signe, alors l'approche donnée ci-dessus serait sûre. D'un autre côté, je suis curieux de savoir si les auteurs de la norme s'attendaient à ce que les implémentations pour le matériel de débordement silencieux à deux compléments sautent les rails en cas de débordement d'une manière qui ne force pas une fin de programme anormale immédiate, mais provoque une perturbation imprévisible des autres calculs.

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