Il n'est pas défini car il modifie x
deux 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:
- Entre les points de séquence, l'ordre d'évaluation n'est pas spécifié.
- 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
x=x++
- Bien défini, ne change pasx
.
Tout d'abord,x
est incrémenté (immédiatement lorsqu'ilx++
est évalué), puis son ancienne valeur est stockée dansx
.x++ + ++x
- Incrémentex
deux fois, évalue à2*x+2
.
Bien que chaque côté puisse être évalué en premier, le résultat est soitx + (x+2)
(côté gauche en premier) ou(x+1) + (x+1)
(côté droit en premier).x = x + (x=3)
- Non spécifié,x
défini surx+3
ou6
.
Si le côté droit est évalué en premier, c'est le casx+3
. Il est également possible que lex=3
premier soit évalué, donc c'est le cas3+3
. Dans les deux cas, l'x=3
affectation se produit immédiatement lorsqu'ellex=3
est évaluée, de sorte que la valeur stockée est remplacée par l'autre affectation.x+=(x=3)
- Bien défini, définix
sur 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èsx=3
, et non en deux parties (lirex
, évaluerx=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.
x
est 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 ===
Pourquoi faire ça?
J'ai essayé d'expliquer dans la section ci-dessus - je veux que les règles C soient simples.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 quea=b[i++]
peuvent encore être optimisées de la même manière.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.
la source
x
à lui-même, et si vous voulez augmenter,x
vous pouvez simplement direx++;
- 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.Réponses:
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
plus lisible que
Étant donné qu'un tel changement est extrêmement fondamental et qu'il rompt avec la base de code existante.
la source
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).
la source
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.++i++
précisément est qu'il est généralement difficile de les distinguer des expressions valides avec des effets secondaires (commea=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.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.
la source
Voyons d'abord la définition d'un comportement indéfini:
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:
Je souligne.
Étant donné une expression comme
les sous - expressions
a++
,--b
,c
et++d
peuvent être évalués dans un ordre quelconque . De plus, les effets secondaires dea++
,--b
et++d
peuvent être appliqués à tout moment avant le point de séquence suivant (IOW, même s'ila++
est évalué avant--b
, il n'est pas garanti qu'ila
sera 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
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.
la source
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
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:
ou
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.
la source
b
avanta
.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.
la source
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 sip1 == p2
(en C99restrict
a é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).la source
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, stockezp1[i]
plus tard. À l'exception des opportunités d'optimisation perdues, qui ne semblent pas importantes, je ne vois aucun problème.x = x++;
n'a pas été écrit maist = x; x++; x = t;
oux=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.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.Dans certains cas, ce type de code a été défini dans la nouvelle norme C ++ 11.
la source
x = ++x
c'est maintenant bien défini (mais pasx = x++
)