43 votes

Pourquoi i = v [i ++] n'est-il pas défini?

À partir du C++ (C++11) standard, §1.9.15 qui traite de la commande de l'évaluation, est l'exemple de code suivant:

void g(int i, int* v) {
    i = v[i++]; // the behavior is undefined
}

Comme indiqué dans le code de l'échantillon, le comportement est indéfini.

(Remarque: La réponse à une autre question légèrement différente de construire i + i++, Pourquoi est-a = i + i++ indéfini et pas non spécifiée comportement, pourrait s'appliquer ici: La réponse est, en substance, que le comportement est indéfini pour les historiques des raisons, et non pas par nécessité. Toutefois, la norme semble impliquer certains justification de ce qui est indéfini - voir la citation ci-dessous. Aussi, celle qui est liée à la question entente indique que le comportement doit être quelconque, alors que, dans cette question, je me demande pourquoi le comportement n'est pas correctement spécifié.)

Le raisonnement donné par la norme du comportement indéfini est comme suit:

Si un effet indésirable sur un scalaire objet est séquencé par rapport à soit un autre effet secondaire sur la même scalaire objet ou d'une valeur de calcul à l'aide de la valeur de la même scalaire objet, le comportement est indéfini.

Dans cet exemple, je pense que la sous-expression i++ serait totalement évaluées avant la sous-expression v[...] est évaluée, et que le résultat de l'évaluation de la sous-expression est - i (avant l'incrément), mais que la valeur de i est la valeur incrémentée après que la sous-expression a été complètement évaluées. Je pense qu'à ce moment (après la sous-expression i++ a été complètement évaluées), l'évaluation v[...] a lieu, suivi par la cession i = ....

Par conséquent, bien que l'incrémentation de l' i est inutile, je pense néanmoins que cela devrait être défini.

Pourquoi est-ce un comportement indéfini?

42voto

Steve Jessop Points 166970

Je pense que la sous-expression i++ serait complètement évalués, avant que la sous-expression de v[...] est évaluée

Mais pourquoi pensez-vous que?

Une des raisons historiques de ce code UB est de permettre à des optimisations du compilateur pour déplacer les effets secondaires un peu partout entre la séquence de points. Les moins de séquence de points, plus les possibilités d'optimiser mais la plus confus programmeurs. Si le code dit:

a = v[i++];

L'intention de la norme est que le code de rayonnement peut être:

a = v[i];
++i;

qui pourrait être de deux directives:

tmp = i;
++i;
a = v[tmp];

serait plus que deux.

Le "code optimisé" sauts a est i, mais la norme permet l'optimisation de toute façon, en disant que le comportement du code d'origine n'est pas défini lors de l' a est i.

La norme pourrait facilement dire qu' i++ doit être évalué avant la cession, comme vous le suggérez. Ensuite, le comportement serait entièrement défini et l'optimisation serait interdite. Mais ce n'est pas la façon dont le C et le C++ de faire des affaires.

Aussi méfiez-vous que de nombreux exemples soulevés lors de ces discussions, le rendre plus facile de dire qu'il n'y a UB autour qu'il ne l'est en général. Cela conduit à des gens en disant que c'est "évident" le comportement doit être défini et l'optimisation de l'interdit. Mais pensez-y:

void g(int *i, int* v, int *dst) {
    *dst = v[(*i)++];
}

Le comportement de cette fonction est définie lors de l' i != dst, et dans ce cas, vous voulez optimisation, vous pouvez obtenir (c'est pourquoi C99 introduit restrict, pour permettre à plus d'optimisations que C89 ou C++ n'). Afin de vous donner l'optimisation, le comportement est indéfini si i == dst. Le C et le C++ normes de la bande de roulement d'une amende de ligne quand il s'agit de l'aliasing, entre un comportement indéfini qui n'est pas prévu par le programmeur, et l'interdiction souhaitable optimisations qui échouent dans certains cas. Le nombre de questions à ce sujet sur suggère DONC que les questionneurs préfère un peu moins d'optimisation et un peu plus le comportement défini, mais il n'est toujours pas simple de tracer la ligne.

Hormis si le comportement est entièrement défini est la question de savoir s'il convient de UB, ou simplement un ordre non spécifié de l'exécution de certaines opérations correspondant à la sous-expressions. La raison C passe pour UB est tout à voir avec l'idée de la séquence de points, et le fait que le compilateur n'a pas besoin de réellement avoir une notion de la valeur d'un objet modifié, jusqu'à la prochaine séquence de point. Donc, plutôt que de contraindre l'optimiseur en disant que "les" les changements de la valeur à un certain point quelconque, la norme dit juste (pour paraphraser): (1) tout code qui repose sur la valeur d'un objet modifié avant le prochain point de séquence, a UB; (2) tout le code qui modifie un objet modifié a UB. Où un "objet modifié" est un objet qui serait ont été modifiés depuis le dernier point de séquence dans un ou plusieurs des ordres juridiques de l'évaluation des sous-expressions.

D'autres langues (par exemple Java) aller tout le chemin et de définir complètement l'ordre de l'expression des effets secondaires, donc il y a certainement un cas contre C de l'approche. C++ n'a tout simplement pas accepter cette affaire.

30voto

Yakk Points 31636

Je vais à la conception d'un pathologiques de l'ordinateur1. C'est un multi-core, à latence élevée, single-thread système avec fil jointures qui fonctionne avec l'octet au niveau des instructions. Si vous faites une demande pour que quelque chose arrive, puis l'ordinateur s'exécute dans sa propre "fil" ou le "groupe") un octet niveau de jeu d'instructions, et un certain nombre de cycles plus tard, l'opération est terminée.

Pendant ce temps, le thread principal de l'exécution se poursuit:

void foo(int v[], int i){
  i = v[i++];
}

devient en pseudo-code:

input variable i // = 0x00000000
input variable v // = &[0xBAADF00D, 0xABABABABAB, 0x10101010]
task get_i_value: GET_VAR_VALUE<int>(i)
reg indx = WAIT(get_i_value)
task write_i++_back: WRITE(i, INC(indx))
task get_v_value: GET_VAR_VALUE<int*>(v)
reg arr = WAIT(get_v_value)
task get_v[i]_value = CALC(arr + sizeof(int)*indx)
reg pval = WAIT(get_v[i]_value)
task read_v[i]_value = LOAD_VALUE<int>(pval)
reg got_value = WAIT(read_v[i]_value)
task write_i_value_again = WRITE(i, got_value)
(discard, discard) = WAIT(write_i++_back, write_i_value_again)

Donc vous l'aurez remarquez que je n'ai pas d'attente sur write_i++_back jusqu'à la fin, en même temps que j'attendais sur write_i_value_again (dont je l'ai chargé à partir d' v[]). Et, en effet, ces écritures sont le seul écrit de mémoire.

Imaginez si l'écriture de la mémoire sont vraiment de la partie lente de cette conception par ordinateur, et qu'ils soient regroupées dans une file d'attente des choses qui peuvent être traités par un mémoire parallèle, la modification de l'unité qui fait des choses par-octet de base.

Si l' write(i, 0x00000001) et write(i, 0xBAADF00D) exécution non ordonnée et en parallèle. Chacun est transformé en octet niveau de l'écriture, et ils sont ordonnés aléatoirement.

Nous terminons par l'écriture d' 0x00 alors 0xBA de l'octet de poids fort, alors 0xAD et 0x00 de l'octet suivant, puis 0xF0 0x00 de l'octet suivant, et enfin 0x0D 0x01 pour l'octet de poids faible. La valeur résultante de la je est - 0xBA000001, ce qui peu s'y attendre, encore serait un résultat valide pour votre indéfini de l'opération.

Maintenant, tout ce que je fait il n'y a pour résultat une valeur quelconque. Nous n'avons pas crasher le système. Mais le compilateur serait libre de le faire complètement indéfini, peut-être l'envoi de deux de ces demandes pour le contrôleur de mémoire à la même adresse dans le même lot d'instructions fait planter le système. Ce serait encore un "valide" pour compiler du C++, et un "valide" environnement d'exécution.

Rappelez-vous, c'est un langage où la restriction de la taille des pointeurs de 8 bits est toujours valable environnement d'exécution. C++ permet de compiler plutôt wonkey cibles.

1: Comme l'indique @SteveJessop de commentaire ci-dessous, la blague, c'est que ce pathologique ordinateur se comporte un peu comme un moderne, ordinateur de bureau, jusqu'à ce que vous obtenez en bas de l'octet au niveau des opérations. Non-atomiques int écrit par un CPU n'est pas rares de certains matériels (comme lorsque l' int n'est pas aligné la façon dont le CPU veut qu'il soit aligné).

24voto

Pete Becker Points 27371

La raison n'est pas seulement historique. Exemple:

 int f(int& i0, int& i1) {
    return i0 + i1++;
}
 

Maintenant, qu'est-ce qui se passe avec cet appel:

 int i = 3;
int j = f(i, i);
 

Il est certainement possible de définir des exigences sur le code en f pour que le résultat de cet appel soit bien défini (Java le fait), mais C et C ++ n'imposent aucune contrainte; cela donne plus de liberté aux optimiseurs.

9voto

Joseph Mansfield Points 59346

Vous spécifiquement référence à la C++11, donc je vais répondre avec le C++11 réponse. Il est, cependant, très proche du C++03 répondre, mais la définition de la séquence est différente.

C++11 définit un séquencée avant la relation entre les évaluations sur un seul thread. Il est non symétrique, transitive et de pair-wise. Si certains d'évaluation A n'est pas séquencée avant que certains d'évaluation B et B est également pas séquencée avant de Un, puis deux évaluations sont séquencé.

L'évaluation d'une expression comprend à la fois les calculs de la valeur (la valeur d'une expression) et d'effets secondaires. Un exemple d'un effet secondaire est la modification d'un objet, qui est le plus important pour répondre à la question. D'autres choses comptent aussi comme des effets secondaires. Si un effet secondaire est séquencé par rapport à un autre effet secondaire ou de la valeur de calcul sur le même objet, alors votre programme a un comportement indéterminé.

Donc, c'est la mise en place. La première règle importante est:

Chaque valeur de calcul et des effets secondaires associés avec une pleine expression est séquencée avant chaque calcul de la valeur et des effets secondaires associés à la prochaine pleine expression à évaluer.

Ainsi, toute pleine expression est pleinement évalués avant la prochaine pleine expression. Dans votre question, nous sommes seulement à faire avec une pleine expression, à savoir l' i = v[i++], de sorte que nous n'avons pas besoin de s'inquiéter à ce sujet. La prochaine règle importante est:

Sauf indication contraire, l'évaluation des opérandes des opérateurs individuels et des sous-expressions des expressions individuelles sont séquencé.

Cela signifie que, en a + b, par exemple, l'évaluation de l' a et b sont non séquencés (ils peuvent être évalués dans n'importe quel ordre). Maintenant, pour notre dernière règle importante:

La valeur des calculs des opérandes d'un opérateur sont séquencée avant de la valeur de calcul du résultat de l'opérateur.

Donc, pour a + b, séquencée avant de les relations peuvent être représentées par un arbre où une flèche représente le séquencée avant la relation:

a + b (value computation)
^   ^
|   |
a   b (value computation)

Si les deux évaluations se produire dans les branches distinctes de l'arbre, ils sont non, si cet arbre montre que les évaluations de l' a et b sont non les uns par rapport aux autres.

Maintenant, laissez-nous faire la même chose pour votre i = v[i++] exemple. Nous utilisons le fait que l' v[i++] est équivalent à *(v + (i++)). Nous utilisons aussi des connaissances supplémentaires sur le séquençage de postfix incrément:

La valeur de calcul de l' ++ expression est séquencée avant la modification de l'opérande de l'objet.

Nous voici donc (un nœud de l'arbre est d'une valeur de calcul, sauf si spécifié comme un effet secondaire):

i = v[i++]
^     ^
|     |
i★  v[i++] = *(v + (i++))
                  ^
                  |
               v + (i++)
               ^     ^
               |     |
               v     ++ (side effect on i)★
                     ^
                     |
                     i

Ici, vous pouvez voir que le côté effet sur i, i++, est dans une branche distincte de l'utilisation de l' i en face de l'opérateur d'affectation (j'ai marqué chacune de ces évaluations, avec une ★). Donc, nous avons certainement un comportement indéterminé! Je recommande fortement le dessin de ces diagrammes, si vous jamais demandé si votre séquençage des évaluations va vous causer des soucis.

Donc, nous en arrivons maintenant à la question sur le fait que la valeur de i avant l'opérateur d'affectation n'a pas d'importance, car nous écrire sur elle de toute façon. Mais en fait, dans le cas général, ce n'est pas vrai. Nous pouvons surcharger l'opérateur d'affectation et de faire usage de la valeur de l'objet avant la cession. La norme ne se soucie pas de ce que nous n'utilisons que les règles sont définies telles que la présence d'un calcul de la valeur non avec un effet secondaire va être un comportement indéterminé. Pas de mais. Ce comportement indéfini est là pour permettre au compilateur d'émettre plus d'un code optimisé. Si l'on ajoute le séquençage de l'opérateur d'affectation, cette optimisation ne peut pas être utilisé.

4voto

Dans cet exemple, je pense que la sous-expression i++ serait complètement évalués, avant que la sous-expression de v[...] est évaluée, et que le résultat de l'évaluation de la sous-expression est i (avant l'incrément), mais que la valeur de i est la valeur incrémentée après que la sous-expression a été complètement évaluées.

L'augmentation de la i++ doivent être évalués avant d'indexation v , et donc avant l'attribution d' i, mais le stockage de la valeur de l'incrément de retour à la mémoire n'a pas besoin d'arriver avant. Dans l'énoncé i = v[i++] il y a deux sous-opérations qui modifient i (c'est à dire finiront provoquant un magasin, à partir d'un registre dans la variable i). L'expression i++ est équivalent à x=i+1, i=x, et il n'est pas nécessaire que les deux opérations doivent avoir lieu de manière séquentielle:

x = i+1;
y = v[i];
i = y;
i = x;

Avec cette expansion, le résultat d' i n'est pas liée à la valeur en v[i]. Sur une autre extension, l' i = x cession pourrait avoir lieu avant l' i = y d'assignation, et le résultat serait i = v[i]

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