Pourquoi x = x ++ n'est-il pas défini?

19

Il n'est pas défini car il modifie xdeux fois entre les points de séquence. La norme dit qu'elle n'est pas définie, donc elle n'est pas définie.
Ça, je le sais.

Mais pourquoi?

Ma compréhension est que l'interdire permet aux compilateurs de mieux optimiser. Cela aurait pu avoir un sens lorsque C a été inventé, mais semble maintenant être un argument faible.
Si nous devions réinventer C aujourd'hui, le ferions-nous de cette façon, ou peut-on faire mieux?
Ou peut-être y a-t-il un problème plus profond, qui rend difficile la définition de règles cohérentes pour de telles expressions, il est donc préférable de les interdire?

Supposons donc que nous devions réinventer C aujourd'hui. Je voudrais suggérer des règles simples pour des expressions telles que x=x++, qui me semblent mieux fonctionner que les règles existantes.
J'aimerais avoir votre avis sur les règles suggérées par rapport aux règles existantes, ou d'autres suggestions.

Règles suggérées:

  1. Entre les points de séquence, l'ordre d'évaluation n'est pas spécifié.
  2. Les effets secondaires se produisent immédiatement.

Aucun comportement indéfini n'est impliqué. Les expressions correspondent à cette valeur ou à cela, mais ne formateront sûrement pas votre disque dur (étrangement, je n'ai jamais vu d'implémentation où x=x++formate le disque dur).

Exemples d'expressions

  1. x=x++- Bien défini, ne change pas x.
    Tout d'abord, xest incrémenté (immédiatement lorsqu'il x++est évalué), puis son ancienne valeur est stockée dans x.

  2. x++ + ++x- Incrémente xdeux fois, évalue à 2*x+2.
    Bien que chaque côté puisse être évalué en premier, le résultat est soit x + (x+2)(côté gauche en premier) ou (x+1) + (x+1)(côté droit en premier).

  3. x = x + (x=3)- Non spécifié, xdéfini sur x+3ou 6.
    Si le côté droit est évalué en premier, c'est le cas x+3. Il est également possible que le x=3premier soit évalué, donc c'est le cas 3+3. Dans les deux cas, l' x=3affectation se produit immédiatement lorsqu'elle x=3est évaluée, de sorte que la valeur stockée est remplacée par l'autre affectation.

  4. x+=(x=3)- Bien défini, défini xsur 6.
    Vous pourriez dire que ce n'est qu'un raccourci pour l'expression ci-dessus.
    Mais je dirais que cela +=doit être exécuté après x=3, et non en deux parties (lire x, évaluer x=3, ajouter et stocker une nouvelle valeur).

Quel est l'avantage?

Certains commentaires ont soulevé ce bon point.
Je ne pense certainement pas que des expressions telles que celles qui x=x++devraient être utilisées dans un code normal.
En fait, je suis beaucoup plus strict que cela - je pense que le seul bon usage pour x++en tant que x++;seul.

Cependant, je pense que les règles linguistiques doivent être aussi simples que possible. Sinon, les programmeurs ne les comprennent tout simplement pas. la règle interdisant de changer une variable deux fois entre des points de séquence est certainement une règle que la plupart des programmeurs ne comprennent pas.

Une règle très basique est la suivante:
si A est valide, et B est valide, et qu'ils sont combinés de manière valide, le résultat est valide.
xest une valeur L valide, x++est une expression valide et =est un moyen valide de combiner une valeur L et une expression, alors comment se x=x++fait-il que ce ne soit pas légal?
La norme C fait ici une exception, et cette exception complique les règles. Vous pouvez rechercher stackoverflow.com et voir à quel point cette exception déroute les gens.
Alors je dis - débarrassez-vous de cette confusion.

=== Résumé des réponses ===

  1. Pourquoi faire ça?
    J'ai essayé d'expliquer dans la section ci-dessus - je veux que les règles C soient simples.

  2. Potentiel d'optimisation:
    cela prend une certaine liberté du compilateur, mais je n'ai rien vu qui m'a convaincu qu'il pourrait être significatif.
    La plupart des optimisations peuvent encore être effectuées. Par exemple, a=3;b=5;peut être réorganisé, même si la norme spécifie l'ordre. Des expressions telles que a=b[i++]peuvent encore être optimisées de la même manière.

  3. Vous ne pouvez pas modifier la norme existante.
    J'avoue, je ne peux pas. Je n'ai jamais pensé pouvoir réellement aller de l'avant et changer les normes et les compilateurs. Je voulais seulement penser si les choses auraient pu être faites différemment.

ugoren
la source
10
Pourquoi est-ce important pour toi? Doit- elle être définie, et si oui, pourquoi? Il ne sert à rien de s’affecter xà lui-même, et si vous voulez augmenter, xvous pouvez simplement dire x++;- pas besoin de l’affectation. Je dirais que cela ne devrait pas être défini simplement parce qu'il serait difficile de se rappeler ce qui est censé se produire.
Caleb
4
Dans mon esprit, c'est une bonne question ("Certains hommes voient les choses telles qu'elles sont et demandent pourquoi, je rêve des choses qui n'ont jamais existé et me demande pourquoi"). C'est (à mon avis) une question purement sur la conception du langage, en utilisant la syntaxe C comme exemple, pas une question sur la syntaxe C. Personnellement, je pense que la raison pour laquelle nous n'avons pas de comportement défini pour des expressions telles que x ++ + ++ x ou x = x ++ est simplement parce qu'il y a une possibilité qu'elles soient mal lues.
Jamie Taylor
5
@ugoren: Pourquoi avez-vous besoin de prédire le résultat. Personne dans son bon sens n'écrirait du code comme ça (comme cela a été mentionné plusieurs fois) même si vous écriviez du code comme celui-ci, il serait rejeté lors de la première révision du code. Il n'est donc pas nécessaire de définir le comportement et de donner à l'optimiseur les meilleures chances de l'optimiser. Dans chaque exemple que vous proposez, je tirerais sur quelqu'un s'il l'ajoutait à la base de code.
Martin York
3
Je trouverais une question plus intéressante à savoir pourquoi n'est-ce pas une erreur d'écrire ceci? Un compilateur pourrait sûrement détecter son comportement indéfini et ne pourrait donc pas être ce que l'utilisateur voulait réellement, alors pourquoi n'est-il pas dans une erreur? Je comprends que certains cas de comportement indéfini sont difficiles à détecter, mais ce n'est pas le cas.
JohnB
3
" la règle interdisant de changer une variable deux fois entre des points de séquence est certainement une règle que la plupart des programmeurs ne comprennent pas. " - Avez-vous des preuves de cela? La plupart des questions que j'ai vues ont été posées parce que les programmeurs ne connaissaient pas la règle. Existe-t-il des preuves que la plupart d'entre eux ne l'ont toujours pas compris après que cela ait été expliqué?
Sécurisé le

Réponses:

24

Peut-être devriez-vous d'abord répondre à la question de savoir pourquoi elle devrait être définie? Y a-t-il un avantage dans le style de programmation, la lisibilité, la maintenabilité ou les performances en permettant de telles expressions avec des effets secondaires supplémentaires? Est

y = x++ + ++x;

plus lisible que

y = 2*x + 2;
x += 2;

Étant donné qu'un tel changement est extrêmement fondamental et qu'il rompt avec la base de code existante.

Sécurise
la source
1
J'ai ajouté une section «pourquoi» à ma question. Je ne suggère certainement pas d'utiliser ces expressions, mais je suis intéressé par des règles simples pour dire la signification d'une expression.
ugoren
En outre, cette modification ne casse pas le code existant, sauf si elle a invoqué un comportement non défini. Corrige moi si je me trompe.
ugoren
3
Eh bien, une réponse plus philosophique: elle n'est actuellement pas définie. Si aucun programmeur ne l'utilise, alors vous n'avez pas besoin de comprendre de telles expressions, car il ne devrait pas y avoir de code. S'il est nécessaire que vous les compreniez, alors il doit évidemment y avoir beaucoup de code qui repose sur un comportement non défini. ;)
Sécurisé le
1
Par définition, il ne casse aucune base de code existante pour définir les comportements. S'ils contenaient de l'UB, ils étaient, par définition, déjà cassés.
DeadMG
1
@ugoren: Votre section "pourquoi" ne répond toujours pas à la question pratique: pourquoi voudriez-vous cette expression bizarre dans votre code? Si vous ne pouvez pas trouver une réponse convaincante à cela, alors toute la discussion est théorique.
Mike Baranczak
20

L'argument selon lequel rendre ce comportement non défini permet une meilleure optimisation n'est pas faible aujourd'hui. En fait, c'est beaucoup plus fort aujourd'hui qu'il ne l'était quand C était nouveau.

Lorsque C était nouveau, les machines qui pouvaient en profiter pour une meilleure optimisation étaient principalement des modèles théoriques. Les gens avaient parlé de la possibilité de construire des CPU où le compilateur indiquerait au CPU quelles instructions pourraient / devraient être exécutées en parallèle avec d'autres instructions. Ils ont souligné le fait que permettre à ce comportement d'avoir un comportement indéfini signifiait que sur un tel processeur, s'il existait vraiment, vous pouviez planifier la partie "incrément" de l'instruction pour qu'elle s'exécute en parallèle avec le reste du flux d'instructions. Alors qu'ils avaient raison sur la théorie, à l'époque il y avait peu de matériel qui pouvait vraiment tirer parti de cette possibilité.

Ce n'est plus seulement théorique. Il existe maintenant du matériel en production et largement utilisé (par exemple, Itanium, DSP VLIW) qui peut vraiment en tirer parti. Ils ont vraiment faire permettre au compilateur de générer un flux d'instructions qui spécifie que les instructions X, Y et Z peuvent tous être exécutés en parallèle. Ce n'est plus un modèle théorique - c'est du vrai matériel en utilisation réelle qui fait un vrai travail.

OMI, rendre ce comportement défini est proche de la pire "solution" possible au problème. Vous ne devez clairement pas utiliser des expressions comme celle-ci. Pour la grande majorité du code, le comportement idéal serait que le compilateur rejette simplement de telles expressions. À l'époque, les compilateurs C n'avaient pas effectué l'analyse de flux nécessaire pour détecter cela de manière fiable. Même à l'époque de la norme C d'origine, ce n'était pas du tout courant.

Je ne suis pas sûr que ce soit acceptable pour la communauté aujourd'hui non plus - alors que de nombreux compilateurs peuvent effectuer ce type d'analyse de flux, ils ne le font généralement que lorsque vous demandez une optimisation. Je doute que la plupart des programmeurs aimeraient l'idée de ralentir les builds de "débogage" juste pour pouvoir rejeter du code qu'ils (étant sain d'esprit) n'écriraient jamais en premier lieu.

Ce que C a fait est un deuxième choix semi-raisonnable: dites aux gens de ne pas le faire, en permettant (mais pas en exigeant) au compilateur de rejeter le code. Cela évite (encore plus) de ralentir la compilation pour les personnes qui ne l'auraient jamais utilisé, mais permet toujours à quelqu'un d'écrire un compilateur qui rejettera ce code s'il le souhaite (et / ou a des indicateurs qui le rejetteront que les gens peuvent choisir d'utiliser). ou pas comme bon leur semble).

Au moins à l'OMI, l'adoption de ce comportement défini serait (au moins proche) la pire décision possible à prendre. Sur le matériel de style VLIW, vous avez le choix de générer du code plus lent pour les utilisations raisonnables des opérateurs d'incrémentation, juste pour le plaisir d'un code merdique qui les abuse, ou sinon vous aurez toujours besoin d'une analyse de flux approfondie pour prouver que vous n'avez pas affaire à code merdique, vous pouvez donc produire le code lent (sérialisé) uniquement lorsque cela est vraiment nécessaire.

Conclusion: si vous voulez résoudre ce problème, vous devriez penser dans la direction opposée. Au lieu de définir ce que fait un tel code, vous devez définir le langage afin que de telles expressions ne soient tout simplement pas autorisées du tout (et vivre avec le fait que la plupart des programmeurs opteront probablement pour une compilation plus rapide plutôt que d'appliquer cette exigence).

Jerry Coffin
la source
OMI, il y a peu de raisons de croire que dans la plupart des cas, les instructions plus lentes sont vraiment beaucoup plus lentes que les instructions rapides et que celles-ci auront toujours un impact sur les performances du programme. Je classerais celui-ci sous optimisation prématurée.
DeadMG
Peut-être que je manque quelque chose - si personne n'est jamais censé écrire un tel code, pourquoi se soucier de l'optimiser?
ugoren
1
@ugoren: écrire du code comme a=b[i++];(pour un exemple) est bien, et l'optimiser est une bonne chose. Cependant, je ne vois pas l'intérêt de nuire à un code raisonnable comme ça, juste pour que quelque chose comme ++i++ait une signification définie.
Jerry Coffin
2
@ugoren Le problème est un problème de diagnostic. Le seul but de ne pas interdire carrément des expressions telles que ++i++précisément est qu'il est généralement difficile de les distinguer des expressions valides avec des effets secondaires (comme a=b[i++]). Cela peut sembler assez simple pour nous, mais si je me souviens bien du Dragon Book, c'est en fait un problème NP-difficile. C'est pourquoi ce comportement est UB, plutôt qu'interdit.
Konrad Rudolph
1
Je ne pense pas que la performance soit un argument valable. J'ai du mal à croire que le cas est assez courant, compte tenu de la différence très mince et de l'exécution très rapide dans les deux cas, pour qu'une petite baisse de performance soit perceptible - sans mentionner que sur de nombreux processeurs et architectures, le définir est effectivement gratuit.
DeadMG
9

Eric Lippert, concepteur principal de l'équipe du compilateur C #, a publié sur son blog un article sur un certain nombre de considérations qui entrent dans le choix de rendre une fonctionnalité non définie au niveau des spécifications de langue. De toute évidence, C # est un langage différent, avec différents facteurs entrant dans sa conception du langage, mais les points qu'il soulève sont néanmoins pertinents.

En particulier, il souligne la question d'avoir des compilateurs existants pour une langue qui ont des implémentations existantes et également des représentants au sein d'un comité. Je ne sais pas si c'est le cas ici, mais a tendance à être pertinent pour la plupart des discussions sur les spécifications liées à C et C ++.

Comme vous l'avez dit, le potentiel de performances pour l'optimisation du compilateur est également à noter. Bien qu'il soit vrai que les performances des processeurs de nos jours sont de plusieurs ordres de grandeur supérieures à ce qu'elles étaient lorsque C était jeune, une grande quantité de programmation C effectuée ces jours-ci est effectuée spécifiquement en raison du gain de performances potentiel et du potentiel de (futur hypothétique ) Les optimisations des instructions CPU et les optimisations de traitement multicœur seraient idiotes à exclure en raison d'un ensemble de règles trop restrictives pour la gestion des effets secondaires et des points de séquence.

Tanzelax
la source
D'après l'article auquel vous créez un lien, il semble que C # ne soit pas loin de ce que je suggère. L'ordre des effets secondaires est défini "lorsqu'il est observé à partir du fil qui provoque les effets secondaires". Je n'ai pas mentionné le multi-threading, mais en général, C ne garantit pas beaucoup pour un observateur dans un autre thread.
ugoren
5

Voyons d'abord la définition d'un comportement indéfini:

3.4.3

1 comportement
indéfini comportement, lors de l'utilisation d'une construction de programme non portable ou erronée ou de données erronées, pour laquelle la présente Norme internationale n'impose aucune exigence

2 NOTE une manière documentée caractéristique de l'environnement (avec ou sans émission d'un message de diagnostic), à la fin d'une traduction ou d'une exécution (avec émission d'un message de diagnostic).

3 EXEMPLE Un exemple de comportement indéfini est le comportement en cas de débordement d'entier

En d'autres termes, un "comportement indéfini" signifie simplement que le compilateur est libre de gérer la situation comme il le souhaite et qu'une telle action est considérée comme "correcte".

La racine du problème en discussion est la clause suivante:

6.5 Expressions

...
3 Le regroupement d'opérateurs et d'opérandes est indiqué par la syntaxe. 74) sauf dans les cas spéci fi ée plus tard (pour la fonction d'appel (), &&, ||, ?:, et les opérateurs par des virgules), l'ordre d'évaluation des sous - expressions et de l'ordre dans lequel les effets secondaires ont lieu sont à la fois fi é eci .

Je souligne.

Étant donné une expression comme

x = a++ * --b / (c + ++d);

les sous - expressions a++, --b, cet ++dpeuvent être évalués dans un ordre quelconque . De plus, les effets secondaires de a++, --bet ++dpeuvent être appliqués à tout moment avant le point de séquence suivant (IOW, même s'il a++est évalué avant --b, il n'est pas garanti qu'il asera mis à jour avant d' --bêtre évalué). Comme d'autres l'ont dit, la raison d'être de ce comportement est de donner à l'implémentation la liberté de réorganiser les opérations de manière optimale.

Pour cette raison, cependant, des expressions comme

x = x++
y = i++ * i++
a[i] = i++
*p++ = -*p    // this one bit me just yesterday

etc., produira des résultats différents pour différentes implémentations (ou pour la même implémentation avec différents paramètres d'optimisation, ou en fonction du code environnant, etc.).

Le comportement n'est pas défini sorte que le compilateur n'a aucune obligation de "faire la bonne chose", quelle qu'elle soit. Les cas ci-dessus sont assez faciles à détecter, mais il existe un nombre non négligeable de cas qui seraient difficiles à impossibles à détecter au moment de la compilation.

De toute évidence, vous pouvez concevoir un langage tel que l'ordre d'évaluation et l'ordre dans lequel les effets secondaires sont appliqués sont strictement définis, et Java et C # le font, principalement pour éviter les problèmes auxquels les définitions C et C ++ conduisent.

Alors, pourquoi cette modification n'a-t-elle pas été apportée à C après 3 révisions standard? Tout d'abord, il y a 40 ans de code C hérité, et il n'est pas garanti qu'un tel changement ne cassera pas ce code. Cela met un peu la charge sur les rédacteurs de compilateurs, car un tel changement rendrait immédiatement tous les compilateurs existants non conformes; tout le monde devrait faire des réécritures importantes. Et même sur des processeurs rapides et modernes, il est toujours possible de réaliser de réels gains de performances en modifiant l'ordre d'évaluation.

John Bode
la source
1
Très bonne explication du problème. Je ne suis pas d'accord sur la rupture des applications héritées - la façon dont un comportement non défini / non spécifié est implémenté change parfois entre les versions du compilateur, sans aucun changement dans la norme. Je ne suggère pas de changer un comportement défini.
ugoren
4

Vous devez d'abord comprendre que ce n'est pas seulement x = x ++ qui n'est pas défini. Personne ne se soucie de x = x ++, car peu importe ce que vous définiriez, cela ne sert à rien. Ce qui n'est pas défini ressemble plus à "a = b ++ où a et b se trouvent être les mêmes" - ie

void f(int *a, int *b) {
    *a = (*b)++;
}
int i;
f(&i, &i);

Il existe plusieurs façons d'implémenter la fonction, selon ce qui est le plus efficace pour l'architecture du processeur (et pour les instructions environnantes, dans le cas où il s'agit d'une fonction plus complexe que l'exemple). Par exemple, deux évidents:

load r1 = *b
copy r2 = r1
increment r1
store *b = r1
store *a = r2

ou

load r1 = *b
store *a = r1
increment r1
store *b = r1

Notez que le premier répertorié ci-dessus, celui qui utilise plus d'instructions et plus de registres, est celui que vous auriez besoin d'être utilisé dans tous les cas où a et b ne peuvent pas être prouvés différents.

Aléatoire832
la source
Vous montrez en effet un cas où ma suggestion entraîne plus d'opérations sur la machine, mais elle me semble insignifiante. Et le compilateur a encore une certaine liberté - la seule vraie exigence que j'ajoute est de stocker bavant a.
ugoren
3

Héritage

L'hypothèse que C pourrait être réinventé aujourd'hui ne peut pas tenir. Il y a tellement de lignes de codes C qui ont été produites et utilisées quotidiennement, que changer les règles du jeu au milieu du jeu est tout simplement faux.

Bien sûr, vous pouvez inventer un nouveau langage, disons C + = , avec vos règles. Mais ce ne sera pas C.

mouviciel
la source
2
Je ne pense pas vraiment que nous puissions réinventer C aujourd'hui. Cela ne signifie pas que nous ne pouvons pas discuter de ces questions. Cependant, ce que je suggère n'est pas vraiment réinventer. La conversion d'un comportement indéfini en comportement défini ou non spécifié peut être effectuée lors de la mise à jour d'une norme, et le langage serait toujours C.
ugoren
2

Déclarer que quelque chose est défini ne changera pas les compilateurs existants pour respecter votre définition. Cela est particulièrement vrai dans le cas d'une hypothèse sur laquelle on peut se fonder explicitement ou implicitement à de nombreux endroits.

Le problème majeur de l'hypothèse n'est pas avec x = x++;(les compilateurs peuvent facilement le vérifier et doivent avertir), c'est avec *p1 = (*p2)++et équivalent ( p1[i] = p2[j]++;lorsque p1 et p2 sont des paramètres d'une fonction) où le compilateur ne peut pas savoir facilement si p1 == p2(en C99 restricta été ajouté pour étendre la possibilité de supposer p1! = p2 entre les points de séquence, il a donc été jugé que les possibilités d'optimisation étaient importantes).

AProgrammer
la source
Je ne vois pas en quoi ma suggestion change quoi que ce soit p1[i]=p2[j]++. Si le compilateur ne peut assumer aucun alias, il n'y a aucun problème. Si ce n'est pas le cas, il doit passer par le livre - incrémentez d' p2[j]abord, stockez p1[i]plus tard. À l'exception des opportunités d'optimisation perdues, qui ne semblent pas importantes, je ne vois aucun problème.
ugoren
Le deuxième paragraphe n'était pas indépendant du premier, mais un exemple du genre d'endroits où l'hypothèse peut s'insinuer et sera difficile à suivre.
Programmeur le
Le premier paragraphe indique quelque chose d'assez évident - les compilateurs devront être modifiés pour se conformer à une nouvelle norme. Je ne pense pas vraiment avoir la chance de standardiser cela et de faire suivre les rédacteurs du compilateur. Je pense juste qu'il vaut la peine d'en discuter.
ugoren
Le problème n'est pas qu'il faut changer les compilateurs à propos de tout changement de langage qui en a besoin, c'est que les changements sont omniprésents et difficiles à trouver. L'approche la plus pratique serait probablement de changer le format intermédiaire sur lequel fonctionne l'optimiseur, c'est-à-dire de prétendre qu'il x = x++;n'a pas été écrit mais t = x; x++; x = t;ou x=x; x++;ou ce que vous voulez comme sémantique (mais qu'en est-il des diagnostics?). Pour une nouvelle langue, supprimez simplement les effets secondaires.
Programmeur le
Je ne connais pas trop la structure du compilateur. Si je voulais vraiment changer tous les compilateurs, je m'en soucierais plus. Mais peut-être que traiter x++comme un point de séquence, comme s'il s'agissait d'un appel de fonction, inc_and_return_old(&x)ferait l'affaire.
ugoren
-1

Dans certains cas, ce type de code a été défini dans la nouvelle norme C ++ 11.

DeadMG
la source
5
Envie d'élaborer?
ugoren
Je pense que x = ++xc'est maintenant bien défini (mais pas x = x++)
MM