Pourquoi f (i = -1, i = -1) n'est-il pas défini?

je lisais à propos de ordre des violations d'évaluation , et ils donnent un exemple qui me laisse perplexe.

1) si un effet secondaire sur un objet scalaire n'est pas séquencé par rapport à un autre effet secondaire sur le même objet scalaire, le comportement n'est pas défini.

// snip
f(i = -1, i = -1); // undefined behavior

dans ce contexte, i est un objet scalaire , qui signifie apparemment

Les types arithmétiques

(3.9.1), les types de dénombrement, les types de pointeurs, les pointeurs vers les types de membres (3.9.2), std::nullptr_t, et les versions qualifiées cv de ces types (3.9.3) sont collectivement appelés types scalaires.

Je ne vois pas en quoi la déclaration est ambiguë dans ce cas. Il me semble que, peu importe si le premier ou le deuxième argument est évalué en premier, i finit -1 , et les deux arguments sont également -1 .

quelqu'un peut-il clarifier?


mise à JOUR

j'apprécie vraiment toute la discussion. Jusqu'à présent, j'aime beaucoup la réponse de @harmic car elle expose les pièges et les subtilités de la définition de cette déclaration en dépit de la façon simple dont il semble à première vue. @acheong87 souligne quelques problèmes qui surgissent lors de l'utilisation références, mais je pense que c'est orthogonal à l'aspect des effets secondaires sans conséquence de cette question.


résumé

puisque cette question a reçu beaucoup d'attention, je vais résumer les principaux points/réponses. Tout d'abord, permettez-moi une petite digression pour souligner que "Pourquoi" peut avoir des significations étroitement liées mais subtilement différentes, à savoir "pour quoi cause ", "pour quoi raison ", et "pour quoi but ". Je regrouperai les réponses par lesquelles de ces significations du "pourquoi" elles s'adressaient.

pour quelle cause

La principale réponse ici provient de Paul Draper , avec Martin J contribuer similaire, mais pas aussi étendues de réponse. La réponse de Paul Draper se résume à

c'est un comportement non défini parce qu'il n'est pas défini ce que le comportement est.

la réponse est globalement très bonne en termes d'explication de ce que dit la norme C++. Il traite également de certains cas connexes D'UB tels que f(++i, ++i); et f(i=1, i=-1); . Dans le premier des cas connexes, Il n'est pas clair si le premier argument devrait être i+1 et le second i+2 ou vice versa; dans le second, il n'est pas clair si i devrait être 1 ou -1 après l'appel de fonction. Ces deux cas sont UB parce que elles relèvent de la règle suivante:

si un effet secondaire sur un objet scalaire n'est pas comparé à un autre effet secondaire sur le même objet scalaire, le comportement n'est pas défini.

par conséquent, f(i=-1, i=-1) est également UB car il relève de la même règle, malgré que l'intention du programmeur est (IMHO) évidente et sans ambiguïté.

Paul Draper le rend également explicite dans sa conclusion que

aurait-il pu définir un comportement? Oui. Était-il défini? Aucun.

ce qui nous amène à la question de "pour quelle raison/but f(i=-1, i=-1) a-t-il été laissé en tant que comportement non défini?"

pour quelles raisons

bien qu'il y ait quelques oublis (peut-être négligents) dans la norme C++, de nombreuses omissions sont bien raisonnées et servent un but spécifique. Même si je suis conscient que le but est souvent soit "rendre le travail du compilateur-rédacteur plus facile", ou "code plus rapide", j'étais principalement intéressé de savoir s'il ya une bonne raison de quitter f(i=-1, i=-1) UB.

harmaline et supercat fournir les principales réponses qui fournissent un raison pour l'UB. Harmaline, un compilateur optimisant que pourriez casser l'assignation atomique ostensiblement des opérations dans de multiples instructions de machine, et qu'il pourrait en outre interleave ces instructions pour la vitesse optimale. Cela pourrait conduire à des résultats surprenants: i finit -2 dans son scénario! Ainsi, harmic démontre comment l'attribution de la même valeur à une variable plus d'une fois peut avoir des effets néfastes si les opérations ne sont pas suivies.

supercat fournit une exposition connexe de la les pièges d'essayer d'obtenir f(i=-1, i=-1) de faire ce qu'il dirait qu'il devrait faire. Il fait remarquer que sur certaines architectures, il y a des restrictions dures contre plusieurs Écritures simultanées à la même adresse mémoire. Un compilateur pourrait avoir du mal à attraper cela si nous avions affaire à quelque chose de moins trivial que f(i=-1, i=-1) .

davidf fournit aussi un exemple d'instructions de sertissage très semblables à celles de harmic.

bien que chacun des exemples d'harmic, de supercat et de davidf soit quelque peu artificiel, pris ensemble, ils servent toujours à fournir une raison tangible pour laquelle f(i=-1, i=-1) devrait être un comportement non défini.

j'ai accepté la réponse de harmic parce qu'il a fait le meilleur travail d'aborder toutes les significations de pourquoi, même si la réponse de Paul Draper a abordé la partie "pour quelle cause" mieux.

autres réponses

JohnB souligne que si nous considérons les opérateurs d'affectation surchargés (au lieu de simples scalaires), alors nous pouvons aussi avoir des problèmes.

258
demandé sur Community 2014-02-10 10:31:32

11 réponses

étant donné que les opérations ne sont pas consécutives, rien ne permet de dire que les instructions qui exécutent la tâche ne peuvent pas être intercalées. Il pourrait être optimal de le faire, en fonction de l'architecture CPU. La page référencée dit ceci:

si A n'est pas séquencé avant B et B n'est pas séquencé avant A, alors deux possibilités existent:

  • les évaluations de A et B ne sont pas suivies: elles peuvent être réalisée dans n'importe quel ordre et peuvent se chevaucher (dans un seul thread d'exécution, le le compilateur peut intercaler les instructions CPU qui comprennent A et B)

  • les évaluations de A et b sont effectuées dans un ordre indéterminé: elles peuvent être effectuées dans n'importe quel ordre mais ne peuvent pas se chevaucher: l'une ou l'autre des évaluations A sera complète. avant B, ou B sera complète avant A. L'ordre peut être le en face de la prochaine fois, la même expression est évaluée.

que par lui - même ne semble pas comme il causerait un problème-en supposant que l'opération effectuée stocke la valeur -1 dans un emplacement de mémoire. Mais il n'y a rien pour dire que le compilateur ne peut pas optimiser cela dans un ensemble séparé d'instructions qui a le même effet, mais qui pourrait échouer si l'opération était entrelacée avec une autre opération sur le même emplacement de mémoire.

par exemple, imaginez qu'il était plus efficace pour zéro la mémoire, puis la décrémenter, par rapport au chargement de la valeur -1 in. Puis ceci:

f(i=-1, i=-1)

pourrait devenir:

clear i
clear i
decr i
decr i

maintenant je suis -2.

C'est probablement un faux exemple, mais c'est possible.

331
répondu harmic 2015-03-24 19:35:37

D'abord, "objet scalaire" signifie un type comme int , float , ou un pointeur (voir Qu'est-ce qu'un objet scalaire en C++? ).


deuxièmement, il peut sembler plus évident que

f(++i, ++i);

aurait un comportement indéfini. Mais

f(i = -1, i = -1);

est moins évident.

un exemple légèrement différent:

int i;
f(i = 1, i = -1);
std::cout << i << "\n";

que s'est-il passé "dernier", i = 1 , ou i = -1 ? Ce n'est pas définie dans la norme. En réalité, cela signifie que i pourrait être 5 (voir la réponse de harmic pour une explication tout à fait plausible de la façon dont cela devrait être le cas). Ou votre programme pourrait segfault. Ou reformater votre disque dur.

mais maintenant vous demandez: "Qu'en est-il de mon exemple? J'ai utilisé la même valeur ( "151990920), pour les deux missions. Qu'est-ce qui pourrait être ambigu à ce sujet?"

vous avez raison...sauf dans la façon dont le Comité des normes c++ l'a décrit.

si un effet secondaire sur un objet scalaire n'est pas comparé à un autre effet secondaire sur le même objet scalaire, le comportement n'est pas défini.

ils auraient pu faire une exception spéciale pour votre cas particulier, mais ils ne l'ont pas fait. (Et pourquoi le devraient-ils? Quelle sera l'utilisation que jamais éventuellement avoir?) Ainsi, i pourrait encore être 5 . Ou ton disque dur pourrait être vide. Ainsi la réponse à votre question Est:

il s'agit d'un comportement non défini car il n'est pas défini ce qu'il est.

(cela mérite d'être souligné car de nombreux programmeurs pensent que" non défini "signifie" aléatoire", ou"imprévisible". Il ne le fait pas; il signifie non défini par la norme. Le comportement pourrait être 100% cohérente, et encore être défini.)

aurait-il pu définir un comportement? Oui. Était-il défini? Aucun. Par conséquent, il est "undefined".

cela dit," non défini "ne signifie pas qu'un compilateur formatera votre disque dur...cela signifie qu'il pourrait et il serait toujours un compilateur conforme aux normes. De façon réaliste, je suis sûr que g++, Clang et MSVC feront tous ce que vous attendiez. Ils ont juste ne serait pas "avoir".


une autre question pourrait être pourquoi le Comité des normes C++ a-t-il choisi de ne pas tenir compte de cet effet secondaire? . Cette réponse portera sur l'histoire et les opinions du Comité. Ou Qu'est-ce qu'il y a de bien à avoir cet effet secondaire sans conséquence en C++? , ce qui permet toute justification, que ce soit ou non le raisonnement du comité de normalisation. Vous pouvez poser ces questions ici, ou à programmers.stackexchange.com.

207
répondu Paul Draper 2017-05-23 12:34:25

une raison pratique de ne pas faire une exception aux règles juste parce que les deux valeurs sont les mêmes:

// config.h
#define VALUEA  1

// defaults.h
#define VALUEB  1

// prog.cpp
f(i = VALUEA, i = VALUEB);

prenons le cas cela était autorisé.

maintenant, quelques mois plus tard, le besoin se fait sentir de changer

 #define VALUEB 2

Apparemment inoffensif, n'est-ce pas? Et puis soudain prog.cpp ne compilerait plus. Pourtant, nous pensons que la compilation ne doit pas dépendre de la valeur d'un littéral.

Conclusion: Il n'y a pas d'exception à la règle parce qu'elle ferait dépendre le succès de la compilation de la valeur (plutôt du type) d'une constante.

EDIT

@HeartWare a souligné que les expressions constantes de la forme A DIV B ne sont pas permises dans certaines langues, quand B est 0, et font échouer la compilation. Par conséquent, le changement d'une constante pourrait causer des erreurs de compilation à un autre endroit. Qui est, à mon humble avis, malheureux. Mais il est certainement bon de limiter ces choses à l'inévitable.

25
répondu Ingo 2017-05-23 11:54:39

comportement est généralement spécifié comme non défini s'il y a une raison concevable pourquoi un compilateur qui essayait d'être "utile" pourrait faire quelque chose qui causerait un comportement totalement inattendu.

dans le cas où une variable est écrite plusieurs fois sans rien pour assurer que les Écritures se produisent à des moments distincts, certains types de matériel pourrait permettre des opérations de" stockage " multiples à effectuer simultanément à des adresses différentes en utilisant une mémoire Double-port. Cependant, certaines mémoires à double port interdisent expressément le scénario où deux magasins frappent simultanément la même adresse, , que les valeurs écrites correspondent ou non à . Si un compilateur pour une telle machine remarque deux tentatives non consécutives d'écrire la même variable, il peut soit refuser de compiler ou s'assurer que les deux écritures ne peuvent pas être programmées simultanément. Mais si l'un ou les deux accès sont via un pointeur ou une référence, le compilateur pourrait ne pas toujours être capable pour dire si les deux Écritures pourraient frapper le même endroit de stockage. Dans ce cas, il pourrait programmer les Écritures simultanément, provoquant un piège matériel sur la tentative d'accès.

bien sûr, le fait que quelqu'un puisse implémenter un compilateur C sur une telle plate-forme ne suggère pas qu'un tel comportement ne devrait pas être défini sur les plates-formes matérielles lorsqu'on utilise des magasins de types assez petits pour être traités atomiquement. En essayant de stocker deux valeurs différentes de façon non-equented pourrait causer des bizarreries si un compilateur n'en est pas conscient; par exemple, donné:

uint8_t v;  // Global

void hey(uint8_t *p)
{
  moo(v=5, (*p)=6);
  zoo(v);
  zoo(v);
}

si le compilateur in-lines l'appel à "moo" et peut dire qu'il ne modifie pas "v", il peut stocker un 5 à v, puis stocker un 6 *p, puis passer 5 à "zoo", et ensuite passer le contenu de v à "zoo". Si "zoo" ne modifie pas " v", il ne devrait y avoir aucun moyen que les deux appels soient passés à des valeurs différentes, mais cela pourrait facilement se produire de toute façon. D'autre part, dans les cas où les deux magasins écriraient la même valeur, une telle bizarrerie ne pouvait pas se produire et il n'y aurait sur la plupart des plateformes aucune raison raisonnable pour une mise en œuvre à faire des choses bizarres. Malheureusement, certains auteurs de compilateurs n'en ont pas besoin. excuse stupide comportements au-delà de "parce que la Norme le permet", de sorte que même ces affaires ne sont pas sûres.

11
répondu supercat 2016-05-26 19:41:09

la confusion est que stocker une valeur constante dans une variable locale n'est pas une instruction atomique sur chaque architecture sur laquelle le C est conçu pour être exécuté. Le processeur sur lequel le code tourne est plus important que le compilateur dans ce cas. Par exemple, sur ARM où chaque instruction ne peut pas porter une constante complète de 32 bits, stocker un int dans une variable nécessite plus d'une instruction. Exemple avec ce pseudo code où vous ne pouvez stocker que 8 bits à la fois et devez travailler dans un 32 bits registre, je est un int32:

reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last

Vous pouvez imaginer que si le compilateur veut optimiser l'on peut intercaler d'une même séquence deux fois, et vous ne savez pas quelle valeur sont écrites dans i; et disons qu'il n'est pas très intelligent:

reg = 0xFF;
reg |= 0xFF00;
reg |= 0xFF0000;
reg = 0xFF;
reg |= 0xFF000000;
i = reg; // writes 0xFF0000FF == -16776961
reg |= 0xFF00;
reg |= 0xFF0000;
reg |= 0xFF000000;
i = reg; // writes 0xFFFFFFFF == -1

cependant dans mes tests gcc est assez aimable pour reconnaître que la même valeur est utilisée deux fois et la génère une fois et ne fait rien de bizarre. Je reçois -1, -1 Mais mon exemple est toujours valable car il est important considérer que même une constante ne peut pas être aussi évidente qu'elle semble être.

10
répondu davidf 2014-02-11 17:25:41

le fait que Le résultat serait le même dans la plupart des implémentations dans ce est accidentel; l'ordre d'évaluation n'est toujours pas défini. Considérez f(i = -1, i = -2) : ici, l'ordre importe. La seule raison pour laquelle il n'importe pas dans votre exemple est l'accident que les deux valeurs sont -1 .

étant donné que l'expression est spécifiée comme un comportement non défini, un compilateur malicieusement conforme peut afficher une image inappropriée lorsque vous évaluez f(i = -1, i = -1) et avortez l'exécution - et encore être considéré comme tout à fait correct. Heureusement, aucun compilateur que je connaisse ne le fait.

9
répondu Amadan 2014-02-11 06:40:52

il me semble que la seule règle concernant le séquençage de l'expression d'argument de fonction est ici:

3) lors de l'appel d'une fonction (que la fonction soit ou non en ligne, et que la syntaxe d'appel de fonction explicite soit ou non utilisée), chaque calcul de valeur et effet secondaire associé à une expression d'argument, ou à l'expression postfix désignant la fonction appelée, est séquencé avant l'exécution de chaque expression ou déclaration dans le le corps de la fonction appelée.

cela ne définit pas le séquençage entre les expressions des arguments, donc nous nous retrouvons dans ce cas:

1) si un effet secondaire sur un objet scalaire n'est pas égalé par rapport à un autre effet secondaire sur le même objet scalaire, le comportement n'est pas défini.

dans la pratique, sur la plupart des compilateurs, l'exemple que vous avez cité fonctionnera très bien (par opposition à "effacer votre disque dur" et autres conséquences d'un comportement non défini).

Il s'agit toutefois d'une responsabilité, car elle dépend du comportement spécifique du compilateur, même si les deux valeurs attribuées sont les mêmes. En outre, évidemment, si vous avez essayé d'attribuer des valeurs différentes, les résultats seraient "vraiment" non défini:

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}
8
répondu Martin J. 2014-02-10 06:52:03

C++17 définit des règles d'évaluation plus strictes. En particulier, il séquence des arguments de fonction (bien que dans un ordre non spécifié).

N5659 §4.6:15

Les évaluations A et B sont séquencées de façon indéterminée lorsque a est séquencé avant B ou B est séquencé avant Un , mais il n'est pas précisé lequel. [ Note : les évaluations séquencées pour une période indéterminée ne peuvent pas se chevaucher, mais l'une ou l'autre pourrait exécuté en premier. - note finale ]

N5659 § 8.2.2:5

Le l'initialisation d'un paramètre, y compris chaque calcul de valeur associé et les effets secondaires, est indéterminée séquencé par rapport à celle de tout autre paramètre.

il permet quelques cas qui seraient UB avant:

f(i = -1, i = -1); // value of i is -1
f(i = -1, i = -2); // value of i is either -1 or -2, but not specified which one
7
répondu AlexD 2017-09-12 22:17:53

de L'opérateur d'affectation pourrait être surchargé, qui pourrait avoir son importance:

struct A {
    bool first;
    A () : first (false) {
    }
    const A & operator = (int i) {
        first = !first;
        return * this;
    }
};

void f (A a1, A a2) {
    // ...
}


// ...
A i;
f (i = -1, i = -1);   // the argument evaluated first has ax.first == true
5
répondu JohnB 2014-02-10 14:08:48

c'est juste une réponse au "Je ne suis pas sûr de ce que" Objet scalaire"pourrait signifier en plus de quelque chose comme un int ou un float".

je interpréter le "scalaire objet" comme une abréviation de "type scalaire objet", ou simplement "variable de type scalaire". Puis, pointer , enum (constante) sont de type scalaire.

C'est un article de MSDN de Types Scalaires .

2
répondu Peng Zhang 2014-02-10 06:56:42

en fait, il y a une raison de ne pas dépendre du fait que le compilateur vérifiera que i est assigné avec la même valeur deux fois, de sorte qu'il est possible de le remplacer par une seule assignation. Et si on avait quelques expressions?

void g(int a, int b, int c, int n) {
    int i;
    // hey, compiler has to prove Fermat's theorem now!
    f(i = 1, i = (ipow(a, n) + ipow(b, n) == ipow(c, n)));
}
1
répondu polkovnikov.ph 2015-04-01 11:38:17