56 votes

Est-ce que a[a[0]] = 1 produit un comportement indéfini ?

Ce code C99 produit-il un comportement indéfini ?

#include <stdio.h>

int main() {
  int a[3] = {0, 0, 0};
  a[a[0]] = 1;
  printf("a[0] = %d\n", a[0]);
  return 0;
}

Dans la déclaration a[a[0]] = 1; , a[0] est à la fois lu et modifié.

J'ai regardé le projet n1124 de l'ISO/IEC 9899. Il est dit (dans 6.5 Expressions) :

Entre le point de séquence précédent et le suivant, la valeur stockée d'un objet est modifiée au maximum une fois par l'évaluation d'une expression. En outre, la valeur antérieure ne doit être lue que pour déterminer la valeur à stocker.

Il ne mentionne pas la lecture d'un objet pour déterminer l'objet lui-même à modifier. Ainsi, cette déclaration pourrait produire un comportement non défini.

Cependant, je le trouve étrange. Est-ce que cela produit réellement un comportement indéfini ?

(Je souhaite également connaître ce problème dans d'autres versions d'ISO C).

52voto

Matt McNabb Points 14273

la valeur antérieure n'est lue que pour déterminer la valeur à mémoriser.

C'est un peu vague et source de confusion, ce qui explique en partie pourquoi la C11 l'a abandonné et a introduit un nouveau modèle de séquençage.

Ce qu'il essaie de dire, c'est que : si la lecture de l'ancienne valeur est garantie pour se produire plus tôt dans le temps que l'écriture de la nouvelle valeur, alors c'est très bien. Sinon, c'est UB. Et bien sûr, il est nécessaire que la nouvelle valeur soit calculée avant d'être écrite.

(Bien sûr, la description que je viens d'écrire sera trouvée par certains comme étant plus vague que le texte standard).

Par exemple x = x + 5 est correcte parce qu'il n'est pas possible de calculer x + 5 sans savoir d'abord x . Cependant a[i] = i++ est faux car la lecture de i sur le côté gauche n'est pas nécessaire pour calculer la nouvelle valeur à stocker dans i . (Les deux lectures de i sont considérés séparément).


Revenons à votre code maintenant. Je pense que c'est un comportement bien défini car la lecture de a[0] afin de déterminer l'index du tableau est garanti avant l'écriture.

Nous ne pouvons pas écrire tant que nous n'avons pas déterminé où écrire. Et nous ne savons pas où écrire tant que nous n'avons pas lu a[0] . Par conséquent, la lecture doit se faire avant l'écriture, il n'y a donc pas d'UB.

Quelqu'un a parlé des points de séquence. Dans C99, il n'y a pas de point de séquence dans cette expression, donc les points de séquence n'entrent pas dans cette discussion.

16voto

haccks Points 33022

Ce code C99 produit-il un comportement indéfini ?

Non. Il ne produira pas de comportement indéfini. a[0] est modifié une seule fois entre deux points de séquence (le premier point de séquence est à la fin de l'initialisateur int a[3] = {0, 0, 0}; et le deuxième est après l'expression complète a[a[0]] = 1 ).

Il ne mentionne pas la lecture d'un objet pour déterminer l'objet lui-même à modifier. Ainsi, cette déclaration pourrait produire un comportement non défini.

Un objet peut être lu plus d'une fois pour se modifier et son comportement est parfaitement défini. Regardez cet exemple

int x = 10;
x = x*x + 2*x + x%5;   

Le deuxième énoncé de la citation dit :

En outre, le valeur préalable ne doit être lu que pour déterminer la valeur à stocker.

Tous les x dans l'expression ci-dessus est lu pour déterminer la valeur de l'objet x lui-même.


NOTE : Notez qu'il y a deux parties de la citation mentionnée dans la question. La première partie dit : Entre le point de séquence précédent et le suivant, la valeur stockée d'un objet est modifiée au maximum une fois par l'évaluation d'une expression. et
donc l'expression comme

i = i++;

relève de UB (deux modifications entre les points de séquence précédents et suivants).

La deuxième partie dit : En outre, la valeur antérieure ne doit être lue que pour déterminer la valeur à stocker. et donc les expressions comme

a[i++] = i;
j = (i = 2) + i;  

invoquer UB. Dans les deux expressions i n'est modifié qu'une seule fois entre le point de séquence précédent et le suivant, mais la lecture du point de séquence le plus à droite i ne déterminent pas la valeur à stocker dans i .


Dans la norme C11, cela a été modifié comme suit

6.5 Expressions :

Si un effet secondaire sur un objet scalaire est non séquencé par rapport à un effet secondaire différent sur le même objet scalaire ou un calcul de valeur utilisant la valeur du même objet scalaire, le comportement est indéfini. [...]

Dans l'expression a[a[0]] = 1 il n'y a qu'un seul effet secondaire à a[0] et le calcul de la valeur de l'indice a[0] est séquencé avant le calcul de la valeur de a[a[0]] .

13voto

John Bollinger Points 16563

C99 présente une énumération de tous les points de séquence dans l'annexe C. Il y en a une à la fin de

a[a[0]] = 1;

car il s'agit d'une expression complète, mais elle ne contient pas de points de séquence. Bien que la logique veuille que la sous-expression a[0] doit être évalué en premier, et le résultat utilisé pour déterminer à quel élément du tableau la valeur est affectée, les règles de séquencement ne le garantissent pas. Lorsque la valeur initiale de a[0] es 0 , a[0] est à la fois lu et écrit entre deux points de séquence, et la lecture est no dans le but de déterminer la valeur à écrire. Selon C99 6.5/2, le comportement de l'évaluation de l'expression est donc indéfini, mais en pratique, je ne pense pas que vous ayez à vous en soucier.

Le C11 est meilleur à cet égard. Le paragraphe 1 de la section 6.5 stipule que

Une expression est une séquence d'opérateurs et d'opérandes qui spécifie le calcul d'une valeur, ou qui désigne un objet ou une fonction, ou qui génère des effets secondaires, ou qui effectue une combinaison de ces éléments. Les calculs de valeur des opérandes d'un opérateur sont séquencés avant le calcul de valeur du résultat de l'opérateur.

Notez en particulier la deuxième phrase, qui n'a pas d'analogue dans C99. On pourrait penser que cela suffit, mais ce n'est pas le cas. Elle s'applique à la calculs de valeur mais il ne dit rien sur la séquence des effets secondaires par rapport aux calculs de la valeur. La mise à jour de la valeur de l'opérande de gauche est un effet secondaire, donc cette phrase supplémentaire ne s'applique pas directement.

La norme C11 nous aide néanmoins sur ce point, car les spécifications des opérateurs d'affectation fournissent le séquencement nécessaire (C11 6.5.16(3)) :

[...] L'effet secondaire de la mise à jour de la valeur stockée de l'opérande de gauche est séquencé après les calculs de valeur des opérandes gauche et droit. Les évaluations des opérandes ne sont pas séquencées.

(En revanche, C99 dit simplement que la mise à jour de la valeur stockée de l'opérande de gauche se produit entre les points de séquence précédents et suivants). Avec les sections 6.5 et 6.5.16 ensemble, alors, C11 donne une séquence bien définie : l'opérande interne [] est évalué avant l'élément extérieur [] qui est évalué avant la mise à jour de la valeur stockée. Cela satisfait la version de C11 de 6.5(2), donc dans C11, le comportement de l'évaluation de l'expression est défini.

5voto

Peter Points 4026

La valeur est bien définie, sauf si a[0] contient une valeur qui n'est pas un indice de tableau valide (c'est-à-dire qui, dans votre code, n'est pas négative et ne dépasse pas 3 ). Vous pourriez changer le code en un code plus lisible et équivalent

 index = a[0];
 a[index] = 1;    /* still UB if index < 0 || index >= 3 */

Dans l'expression a[a[0]] = 1 il est nécessaire d'évaluer a[0] d'abord. Si a[0] est égal à zéro, alors a[0] sera modifié. Mais il n'y a aucun moyen pour un compilateur (à moins de ne pas respecter la norme) de changer l'ordre des évaluations et de modifier les éléments suivants a[0] avant de tenter de lire sa valeur.

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