+ = opérateur pour uint16 t promeut la Valeur assignée à int et ne compile pas

C'est un vrai WTF pour moi, ressemble à un bug dans GCC, mais j'aimerais que la communauté jette un coup d'oeil et trouve une solution pour moi.

Voici le programme le plus simple que je pourrais rassembler:

#include <stdio.h>
#include <stdint.h>

int main(void)
{
 uint16_t i = 1;
 uint16_t j = 2;
 i += j;
 return i;
}

J'essaie de compiler ceci sur GCC avec le drapeau -Werror=conversion, que j'utilise pour une grande partie de mon code.

Voici le résultat:

.code.tio.c: In function ‘main’:
.code.tio.c:9:7: error: conversion to ‘uint16_t {aka short unsigned int}’ from ‘int’ may alter its value [-Werror=conversion]
  i += j;

La même erreur se produirait pour ce code:

uint16_t i = 1;
i += ((uint16_t)3);

L'erreur est

.code.tio.c: In function ‘main’:
.code.tio.c:7:7: error: conversion to ‘uint16_t {aka short unsigned int}’ from ‘int’ may alter its value [-Werror=conversion]
  i += ((uint16_t)3);
       ^

Juste pour être clair, l'erreur est ici le += opérateur, de ne PAS le casting.

Il semble que la surcharge de l'opérateur pour le += avec uint16_t soit foiré. Ou est-ce que je manque quelque chose de subtil ici?

Pour votre usage: MCVE

Edit: un peu plus de la même chose:

.code.tio.c:8:6: error: conversion to ‘uint16_t {aka short unsigned int}’ from ‘int’ may alter its value [-Werror=conversion]
  i = i + ((uint16_t)3);

, Mais i = (uint16_t)(i +3);, au moins travaille...

43
c gcc
demandé sur dbush 2017-11-21 18:43:35

5 réponses

La raison de la conversion implicite est due à l'équivalence de l'opérateur += avec = et +.

De la section 6.5.16.2 de la norme C :

3 une affectation composée de la forme E1 op= E2 est équivalente à l'expression d'affectation simple E1 = E1 op (E2), sauf que la valeur l E1 n'est évaluée qu'une seule fois, et par rapport à appel de fonction séquencé de façon indéterminée, l'opération d'une affectation composée est un évaluation

Donc ceci:

i += ((uint16_t)3);

Est équivalent à:

i = i + ((uint16_t)3);

Dans cette expression, les opérandes de l'opérateur + sont promus à int, et que int est assigné à un uint16_t.

La Section 6.3.1.1 détaille la raison de ceci:

2 Les éléments suivants peuvent être utilisés dans une expression où un int ou unsigned int peut être utilisé:

  • un objet ou une expression avec un type entier (autre que int ou unsigned int) dont le rang de conversion entier est inférieur ou égal à le rang de int et unsigned int.
  • Un peu de champ de type _Bool, int, signed int, ou unsigned int.

Si un {[7] } peut représenter toutes les valeurs du type d'origine (comme restreint par la largeur, pour un champ de bits), la valeur est convertie en int; sinon, il est converti en unsigned int. Ceux-ci sont appelés le promotions entières . Tous les autres types sont inchangés par l'entier Promotion.

Comme un uint16_t (alias un unsigned short int) a un rang inférieur à int, les valeurs sont promues lorsqu'elles sont utilisées comme opérandes à +.

Vous pouvez contourner cela en brisant l'opérateur += et en lançant le côté droit. En outre, en raison de la promotion, la distribution sur la valeur 3 n'a aucun effet, ce qui peut être supprimé:

i =  (uint16_t)(i + 3);

Notez cependant que cette opération est sujette à débordement, ce qui est l'une des raisons pour lesquelles un avertissement est donné lorsqu'il n'y a pas de distribution. Par exemple, si i a la valeur 65535, alors i + 3 a le type int et la valeur 65538. Lorsque le résultat est renvoyé à uint16_t, la valeur 65536 est soustraite de cette valeur donnant la valeur 2, qui est ensuite assignée à i.

Ce comportement est bien défini dans ce cas car le type de destination n'est pas signé. Si le type de destination était signé, le résultat serait implémentation définie.

32
répondu dbush 2017-11-21 18:02:02

L'argument de tout opérateur arithmétique est soumis aux conversions arithmétiques habituelles décrites dans N1570 (dernière ébauche de C11), §6.3.1.8. Le passage pertinent à cette question Est le suivant:

[ quelques règles sur les types à virgule flottante]

Sinon, les promotions entières sont effectuées sur les deux opérandes.

Donc, en regardant plus loin comment lespromotions entières sont définies, nous trouvons le texte pertinent dans §6.3.1.1 p2:

Si un {[1] } peut représenter toutes les valeurs du type d'origine (comme limité par la largeur, pour un bits), la valeur est convertie en une int; sinon, il est converti unsigned int. Ce sont les promotions entières.

Donc, même avec ce code:

i += ((uint16_t)3);

La présence d'un opérateur arithmétique provoque l'opérande d'être reconverti à int. Comme l'affectation fait partie de l'opération, elle attribue un int à i.

Ceci est en effet pertinent car {[7] } pourrait en fait déborder un uint16_t.

13
répondu 2017-11-21 15:58:04
i += ((uint16_t)3);

Est égal à(1)

i = i + ((uint16_t)3);

L'opérande le plus à droite est explicitement converti de int (le type de la constante entière 3) à uint16_t par la distribution. Après cela, les conversions arithmétiques habituelles(2) sont appliqués sur les deux opérandes de +, après quoi les deux opérandes sont implicitement convertis en int. Le résultat de l'opération + est de type int.

Vous essayez ensuite de stocker un int dans un {[5] } qui entraîne correctement un avertissement de -Wconversion.

Un contournement possible si vous souhaitez éviter d'assigner un int à un {[5] } serait quelque chose comme ceci (conforme à MISRA-C, etc.):

i = (uint16_t)(i + 3u);

(1) Ceci est obligatoire pour tous les opérateurs d'affectation composés, C11 6.5.16.2:

Un composé d'affectation de la forme E1 op= E2 est équivalent à la simple expression d'affectation E1 = E1 op (E2), sauf que le lvalue E1 n'est évaluée qu'une seule fois,

(2) Voir Implicite des règles de promotion de type pour plus d'infos sur l'implicite du type de promotions.

11
répondu Lundin 2017-11-21 16:10:28

Une explication se trouve ici:

Joseph [at]codesourcery.com 2009-07-15 14: 15: 38 UTC
Subject: Re: - Wconversion: ne pas avertir pour les opérandes ne dépassant pas le type cible

Le 15 juillet 2009, ian at airs dot com a écrit: {[8]]}

> Bien sûr, il peut envelopper, mais-Wconversion n'est pas pour envelopper les Avertissements.

C'est pour les avertissements sur les conversions implicites changeant une valeur; le l'arithmétique, dans un type plus large (délibérément ou non), fait emballez pas, mais la valeur est modifiée par la conversion implicite en char. Si l'utilisateur avait des conversions explicites à int dans son arithmétique, il ne pouvait pas y avoir de doute que la mise en garde est appropriée.

L'avertissement se produit parce que le compilateur a l'ordinateur effectue l'arithmétique en utilisant un type plus grand que uint16_t (Un int, par la promotion entière), et le placement de la valeur dans un uint16_t pourrait le tronquer. Par exemple,

uint16_t i = 0xFFFF;
i += (uint16_t)3;     /* Truncated as per the warning */

La même chose s'applique à séparer opérateurs d'affectation et d'addition.

uint16_t i = 0xFFFF;
i = i + (uint16_t)3;  /* Truncated as per the warning */
3
répondu ikegami 2017-11-21 15:56:47

Il existe de nombreux cas où il serait utile d'effectuer des opérations entières directement sur de petits types entiers non signés. Étant donné que le comportement de ushort1 = ushort1+intVal; sera dans tous les cas définis équivalent à contraindre intVal au type de ushort1, puis à effectuer l'addition directement sur ce type, les auteurs de la norme n'ont pas vu le besoin d'écrire des règles spéciales pour cette situation. Je pense qu'ils ont clairement reconnu qu'un tel comportement était utile, mais qu'ils s'attendaient à ce que les implémentations se comportent généralement de cette manière, que la norme l'exige ou non.

Incidemment, gcc traite parfois l'arithmétique sur des valeurs de type uint16_t de différentes manières lorsque le résultat est contraint à uint16_t que dans les cas où il ne l'est pas. par exemple, donné

uint32_t multest1(uint16_t x, uint16_t y)
{
  x*=y;
  return x;
}

uint32_t multest2(uint16_t x, uint16_t y)
{
  return (x*y) & 65535u;
}

La fonction multest1() semble exécuter systématiquement le mod de multiplication 65536 dans tous les cas, mais la fonction multest2 ne le fait pas. Par exemple, la fonction:

void tester(uint16_t n, uint16_t *p)
{
  n|=0x8000;
  for (uint16_t i=0x8000; i<n; i++)
    *p++= multest2(65535,i);
  return 0;
}

Sera optimisé équivalent à:

void tester(uint16_t n, uint16_t *p)
{
  n|=0x8000;
  if (n != 0x8000)
    *p++= 0x8000;
  return 0;
}

Mais une telle simplification ne se produira pas lors de l'utilisation de multest1. Je ne considérerais pas le comportement mod-65536 de gcc comme fiable, mais la différence de génération de code montre que:

  1. Certains compilateurs, y compris gcc, effectuent l'arithmétique mod 65536 directement lorsqu'un résultat sera contraint directement à uint16_t, mais...

  2. Certains compilateurs traitent le débordement d'entiers de manière à provoquer un comportement erroné même si le code ignore complètement tous les bits supérieurs du résultat, donc un compilateur qui essaie d'avertir de tous les UB possibles devrait marquer les constructions que les compilateurs seraient autorisés à traiter de manière stupide comme des violations de portabilité.

Bien qu'il existe de nombreuses instructions de la forme ushort1+ = intval qui ne pourraient pas causer de débordement, il est plus facile de grincer de telles instructions que d'identifier uniquement celles qui pourraient réellement causer un comportement erroné.

2
répondu supercat 2017-11-21 16:52:55