69 votes

Quels sont les comportements non définis/non spécifiés les plus courants en C que vous rencontrez ?

Un exemple de comportement non spécifié dans le langage C est l'ordre d'évaluation des arguments d'une fonction. Il peut être de gauche à droite ou de droite à gauche, vous ne le savez pas. Cela affecte la façon dont foo(c++, c) ou foo(++c, c) est évaluée.

Quel autre comportement non spécifié peut surprendre le programmeur non averti ?

4 votes

foo(c++, c) y foo(++c, c) sont tous deux comportement non défini qui l'emporte complètement sur le non spécifié.

80voto

Nils Pipenbrinck Points 41006

Question d'un juriste linguiste. D'accord.

Mon top 3 personnel :

  1. violation de la règle stricte de l'aliasing

  2. violation de la règle stricte de l'aliasing

  3. violation de la règle stricte de l'aliasing

    -)

Editer Voici un petit exemple qui se trompe deux fois :

(en supposant qu'il s'agit d'ints 32 bits et de little endian)

float funky_float_abs (float a)
{
  unsigned int temp = *(unsigned int *)&a;
  temp &= 0x7fffffff;
  return *(float *)&temp;
}

Ce code tente d'obtenir la valeur absolue d'un flottant en jouant sur le bit de signe directement dans la représentation d'un flottant.

Cependant, le résultat de la création d'un pointeur sur un objet en passant d'un type à un autre n'est pas valide en C. Le compilateur peut supposer que des pointeurs de types différents ne pointent pas vers le même morceau de mémoire. Ceci est vrai pour tous les types de pointeurs sauf void* et char* (le signe n'a pas d'importance).

Dans le cas ci-dessus, je le fais deux fois. Une fois pour obtenir un int-alias pour le float a, et une fois pour reconvertir la valeur en float.

Il existe trois façons valables de procéder.

Utiliser un pointeur char ou void lors de la conversion. Ces pointeurs sont toujours des alias de n'importe quoi, ils sont donc sûrs.

float funky_float_abs (float a)
{
  float temp_float = a;
  // valid, because it's a char pointer. These are special.
  unsigned char * temp = (unsigned char *)&temp_float;
  temp[3] &= 0x7f;
  return temp_float;
}

Utiliser memcopy. Memcpy prend des pointeurs de type void, ce qui force également l'aliasing.

float funky_float_abs (float a)
{
  int i;
  float result;
  memcpy (&i, &a, sizeof (int));
  i &= 0x7fffffff;
  memcpy (&result, &i, sizeof (int));
  return result;
}

Troisième solution valable : utiliser les unions. Il s'agit explicitement d'une pas indéfini depuis C99 :

float funky_float_abs (float a)
{
  union 
  {
     unsigned int i;
     float f;
  } cast_helper;

  cast_helper.f = a;
  cast_helper.i &= 0x7fffffff;
  return cast_helper.f;
}

0 votes

Cela semble intéressant... pouvez-vous nous en dire plus ?

0 votes

Aehm. J'ai mentionné que je supposais des ints 32 bits et little endian. Par ailleurs, l'utilisation de l'union est toujours un comportement non défini, non pas à cause de la représentation en bits de l'IEEE, mais simplement parce que vous n'êtes (en théorie) pas autorisé à écrire dans le champ f et à lire dans le champ i.

0 votes

Onebyone, c'est un comportement non défini même si l'implémentation utilise ieee. le point est qu'il lit à partir d'un membre différent de celui dans lequel il a été écrit pour la dernière fois.

31voto

Steve Jessop Points 166970

Mon comportement indéfini préféré est que si un fichier source non vide ne se termine pas par une nouvelle ligne, le comportement est indéfini.

Je pense qu'il est vrai qu'aucun compilateur que j'ai jamais vu n'a traité un fichier source différemment selon qu'il était ou non terminé par une nouvelle ligne, si ce n'est pour émettre un avertissement. Ce n'est donc pas vraiment quelque chose qui surprendra les programmeurs non sensibilisés, si ce n'est qu'ils pourraient être surpris par l'avertissement.

Ainsi, pour les véritables problèmes de portabilité (qui dépendent le plus souvent de l'implémentation plutôt que d'être non spécifiés ou non définis, mais je pense que cela s'inscrit dans l'esprit de la question) :

  • char n'est pas nécessairement (non) signé.
  • int peut être de n'importe quelle taille à partir de 16 bits.
  • ne sont pas nécessairement formatés ou conformes à l'IEEE.
  • les types de nombres entiers ne sont pas nécessairement des compléments à deux, et le dépassement de capacité arithmétique des nombres entiers entraîne un comportement non défini (le matériel moderne ne se bloque pas, mais certaines optimisations du compilateur entraînent un comportement différent de l'enveloppement, même si c'est ce que fait le matériel). Par exemple if (x+1 < x) peut être optimisé comme étant toujours faux lorsque x a un type signé : voir -fstrict-overflow dans GCC).
  • "/", "." et " " dans un #include n'ont pas de signification définie et peuvent être traités différemment par différents compilateurs (cela varie effectivement, et si cela se passe mal, cela vous gâchera la journée).

Des problèmes très sérieux qui peuvent être surprenants même sur la plateforme sur laquelle vous avez développé, parce que le comportement n'est que partiellement non défini / non spécifié :

  • Le threading POSIX et le modèle de mémoire ANSI. L'accès simultané à la mémoire n'est pas aussi bien défini que les novices le pensent. volatile ne fait pas ce que les novices pensent. L'ordre des accès à la mémoire n'est pas aussi bien défini que les novices le pensent. Accès peut peuvent être déplacés à travers les barrières de mémoire dans certaines directions. La cohérence de la mémoire cache n'est pas nécessaire.

  • Profiler un code n'est pas aussi simple qu'on le pense. Si votre boucle de test n'a aucun effet, le compilateur peut la supprimer en tout ou en partie. inline n'a pas d'effet défini.

Et, comme l'a mentionné Nils en passant, je crois :

  • VIOLANT LA RÈGLE STRICTE DE L'ALIASING.

0 votes

Steve - J'ai rencontré exactement ce que vous décrivez (le problème de la fin de la nouvelle ligne) au début des années 90 avec le compilateur Microtec pour la famille 68K. Je pensais que l'outil était bogué, mais j'ai simplement ajouté la nouvelle ligne "pour contourner cet outil stupide". Contrairement à mon collègue trop confiant (voir mon autre commentaire à ce sujet), je n'étais pas si sûr de moi que j'aurais rédigé un rapport de défaut... heureusement que je ne l'ai pas fait.

1 votes

Le débordement d'un entier signé n'est pas seulement une question de pédanterie ; au moins GCC applique des optimisations en supposant que cela n'arrive jamais, comme "if (a + 1 > a)" qui passe toujours et ne détecte jamais le débordement.

0 votes

@BCoates : Je n'ai aucun problème avec les débordements d'entiers produisant des valeurs partiellement indéterminées, ce qui serait une sémantique suffisante pour justifier l'optimisation de GCC dans le cas indiqué. Malheureusement, certains auteurs de compilateurs semblent penser que les débordements d'entiers devraient nier les lois du temps et de la causalité (le temps, je pourrais peut-être l'accepter, si le code était remis en séquence en supposant qu'il ne déborde pas ; la négation de la causalité devrait être considérée comme de la folie, mais hélas, tout le monde n'est pas d'accord).

21voto

Adam Pierce Points 12801

Diviser quelque chose par un pointeur vers quelque chose. Cela ne compile pas pour une raison ou une autre... :-)

result = x/*y;

1 votes

Haha bien joué, je le note :-)

0 votes

Parce que '/*' est menacé comme un commentaire, il suffit d'ajouter un espace entre '/' et '*' et cela devrait fonctionner (du moins cela fonctionne sur mon gcc 8.1.1).

1 votes

Qu'est-ce que c'est que cette réponse incroyablement hilarante et incorrecte ? Elle ne répond pas du tout à la question et suggère des hypothèses erronées sur le code C. Le code donné est une erreur de syntaxe. Cela n'a rien à voir avec un comportement indéfini. - Peut-être vouliez-vous diviser une valeur de type basique par une valeur de pointeur, mais ce n'est pas ce que vous avez montré. Diviser une valeur d'un type de base par un pointeur déréférencé n'est pas incorrect comme par exemple : double x = 2; int z = 1, *y; y = &z; int result = x / *y; - Cette réponse doit être modifiée en profondeur ou supprimée d'urgence. -1

21voto

1800 INFORMATION Points 55907

Ma préférée est celle-ci :

// what does this do?
x = x++;

Pour répondre à certains commentaires, il s'agit d'un comportement non défini selon la norme. En voyant cela, le compilateur est autorisé à faire n'importe quoi, jusqu'à formater votre disque dur. Voir par exemple ce commentaire ici . Il ne s'agit pas de savoir si l'on peut raisonnablement s'attendre à un certain comportement. En raison de la norme C++ et de la manière dont les points de séquence sont définis, cette ligne de code est en fait un comportement non défini.

Par exemple, si nous avions x = 1 avant la ligne ci-dessus, quel serait le résultat valide après ? Quelqu'un a fait remarquer qu'il devrait être

x est incrémenté de 1

de sorte que nous devrions voir x == 2 par la suite. Cependant, ce n'est pas vrai, vous trouverez des compilateurs qui ont x == 1 après, ou peut-être même x == 3. Il faudrait regarder de près l'assemblage généré pour voir pourquoi, mais les différences sont dues au problème sous-jacent. Essentiellement, je pense que c'est parce que le compilateur est autorisé à évaluer les deux instructions d'affectation dans l'ordre qu'il souhaite, de sorte qu'il pourrait faire l'opération suivante x++ d'abord, ou le x = d'abord.

0 votes

X est incrémenté de 1. Vous avez assigné x à lui-même et l'avez ensuite incrémenté. c'est équivalent à x++ ;

0 votes

Ceci fait x = x ; x += 1 ; donc oui, comme le dit Charles Graham. Je n'appellerais pas cela "non spécifié".

17 votes

La modification d'une variable plus d'une fois entre deux points de séquence est explicitement considérée comme un comportement non défini dans les normes C et C++.

7voto

mbac32768 Points 3830

Un compilateur n'a pas à vous dire que vous appelez une fonction avec un mauvais nombre de paramètres ou un mauvais type de paramètres si le prototype de la fonction n'est pas disponible.

0 votes

Cependant, les compilateurs bienveillants vous aideront généralement en vous avertissant...

0 votes

Depuis le C99, l'appel d'une fonction sans déclaration visible nécessite un diagnostic. Cette déclaration ne tienen d'être un prototype (c'est-à-dire une déclaration qui spécifie les types des paramètres), mais il est toujours devrait être. (Les fonctions variables telles que printf peut encore poser problème).

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