Pourquoi f (i = -1, i = -1) un comportement indéfini?

267

Je lisais sur l' 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, iest un objet scalaire , ce qui signifie apparemment

Les types arithmétiques (3.9.1), les types d'énumération, les types pointeur, le pointeur sur les types 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, ifinit par -1, et les deux arguments le sont également -1.

Quelqu'un peut-il clarifier?


METTRE À 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 malgré sa simplicité à première vue. @ acheong87 souligne certains problèmes qui surviennent lors de l'utilisation de références, mais je pense que c'est orthogonal à l'aspect des effets secondaires non séquencés de cette question.


RÉSUMÉ

Étant donné que cette question a retenu l'attention, je vais résumer les principaux points / réponses. Tout d'abord, permettez-moi de faire une petite digression pour souligner que "pourquoi" peut avoir des significations étroitement liées mais subtilement différentes, à savoir "pour quelle cause ", "pour quelle raison " et "dans quel but ". Je vais regrouper les réponses par lesquelles de ces significations de «pourquoi» elles ont abordées.

pour quelle cause

La réponse principale ici vient de Paul Draper , avec Martin J apportant une réponse similaire mais pas aussi complète. La réponse de Paul Draper se résume à

Il s'agit d'un comportement indéfini car il n'est pas défini quel est le comportement.

La réponse est globalement très bonne pour expliquer ce que dit la norme C ++. Il aborde également 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 doit être i+1et le second i+2ou vice versa; dans le second, il n'est pas clair si idoit être 1 ou -1 après l'appel de fonction. Ces deux cas sont UB car ils relèvent de la règle suivante:

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.

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 soit (à mon humble avis) évidente et sans ambiguïté.

Paul Draper précise également dans sa conclusion que

Serait-ce un comportement défini? Oui. At-il été défini? Non.

ce qui nous amène à la question de "pour quelle raison / but a été f(i=-1, i=-1)laissé comme comportement indéfini?"

pour quelle raison / but

Bien qu'il y ait quelques oublis (peut-être négligents) dans la norme C ++, de nombreuses omissions sont bien motivées et servent un objectif spécifique. Bien que je sois conscient que le but est souvent soit de "faciliter le travail du compilateur-rédacteur", soit de "code plus rapide", j'étais surtout intéressé de savoir s'il y avait une bonne raison de quitter f(i=-1, i=-1) UB.

harmic et supercat fournissent les principales réponses qui fournissent une raison pour l'UB. Harmic souligne qu'un compilateur d'optimisation qui pourrait diviser les opérations d'affectation apparemment atomiques en plusieurs instructions machine, et qu'il pourrait encore entrelacer ces instructions pour une vitesse optimale. Cela pourrait conduire à des résultats très surprenants: ifinit comme -2 dans son scénario! Ainsi, harmic 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 séquencées.

supercat fournit une exposition connexe des embûches d'essayer f(i=-1, i=-1)de faire ce qu'il semble devoir faire. Il souligne que sur certaines architectures, il existe des restrictions strictes contre plusieurs écritures simultanées vers la même adresse mémoire. Un compilateur pourrait avoir du mal à comprendre cela si nous avions affaire à quelque chose de moins trivial que f(i=-1, i=-1).

davidf fournit également un exemple d'instructions d'entrelacement très similaires à celles d'Harmic.

Bien que chacun des exemples de Harmic, Supercat et Davidf soit quelque peu artificiel, pris ensemble, ils servent toujours à fournir une raison tangible pour laquelle f(i=-1, i=-1)un comportement devrait être indéfini.

J'ai accepté la réponse d'Harmic parce qu'elle a fait le meilleur travail pour aborder toutes les significations du pourquoi, même si la réponse de Paul Draper abordait mieux la partie "pour quelle cause".

autres réponses

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

Nicu Stiurca
la source
1
Un objet scalaire est un objet de type scalaire. Voir 3.9 / 9: "Les types arithmétiques (3.9.1), les types d'énumération, les types de pointeur, le pointeur vers les types de membre (3.9.2) std::nullptr_tet les versions qualifiées cv de ces types (3.9.3) sont collectivement appelés types scalaires . "
Rob Kennedy
1
Il y a peut-être une erreur sur la page, et ils signifiaient en fait f(i-1, i = -1)ou quelque chose de similaire.
M. Lister
Jetez un oeil à cette question: stackoverflow.com/a/4177063/71074
Robert
@RobKennedy Merci. Les "types arithmétiques" incluent-ils le booléen?
Nicu Stiurca
1
SchighSchagh votre mise à jour devrait être dans la section réponse.
Grijesh Chauhan

Réponses:

343

Les opérations n'étant pas séquencées, rien ne permet de dire que les instructions d'exécution de la mission ne peuvent pas être entrelacées. Il peut être optimal de le faire, selon l'architecture du processeur. La page référencée indique 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 séquencées: elles peuvent être effectuées dans n'importe quel ordre et peuvent se chevaucher (dans un seul thread d'exécution, le compilateur peut entrelacer les instructions CPU qui comprennent A et B)

  • les évaluations de A et B sont séquencées de façon indéterminée: elles peuvent être effectuées dans n'importe quel ordre mais ne peuvent pas se chevaucher: soit A sera terminé avant B, soit B sera terminé avant A. L'ordre peut être l'inverse la prochaine fois avec la même expression est évalué.

En soi, cela ne semble pas poser de problème - en supposant que l'opération effectuée stocke la valeur -1 dans un emplacement mémoire. Mais rien ne dit non plus que le compilateur ne peut pas optimiser cela en un ensemble d'instructions distinct 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 mémoire.

Par exemple, imaginez qu'il était plus efficace de mettre à zéro la mémoire, puis de la décrémenter, par rapport au chargement de la valeur -1 in. Ensuite, 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.

Harmique
la source
59
Très bel exemple de la façon dont l'expression pourrait réellement faire quelque chose d'inattendu tout en se conformant aux règles de séquencement. Oui, un peu artificiel, mais il en est de même du code coupé que je demande en premier lieu. :)
Nicu Stiurca
10
Et même si l'affectation se fait comme une opération atomique, il est possible de concevoir une architecture superscalaire où les deux affectations sont effectuées simultanément provoquant un conflit d'accès à la mémoire qui entraîne un échec. Le langage est conçu pour que les rédacteurs du compilateur aient autant de liberté que possible dans l'utilisation des avantages de la machine cible.
ach
11
J'aime vraiment votre exemple de la façon dont même attribuer la même valeur à la même variable dans les deux paramètres pourrait entraîner un résultat inattendu car les deux affectations ne sont pas séquencées
Martin J.
1
+ 1e + 6 (ok, +1) pour le point que le code compilé n'est pas toujours ce que vous attendez. Les optimiseurs sont vraiment bons pour vous lancer ce genre de courbes lorsque vous ne respectez pas les règles: P
Corey
3
Sur le processeur Arm, une charge de 32 bits peut prendre jusqu'à 4 instructions: elle le fait load 8bit immediate and shiftjusqu'à 4 fois. Habituellement, le compilateur fera un adressage indirect pour récupérer un nombre à partir d'une table pour éviter cela. (-1 peut être fait en 1 instruction, mais un autre exemple pourrait être choisi).
ctrl-alt-delor
208

Tout d'abord, "objet scalaire" signifie un type comme un int, floatou 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";

Quelle affectation s'est produite "en dernier" i = 1, ou i = -1? Ce n'est pas défini dans la norme. Vraiment, ce moyen ipourrait l'être 5(voir la réponse de Harmic pour une explication complètement plausible de la façon dont cela pourrait être le cas). Ou votre programme pourrait segfault. Ou reformatez votre disque dur.

Mais maintenant, vous demandez: "Qu'en est-il de mon exemple? J'ai utilisé la même valeur ( -1) pour les deux affectations. Qu'est-ce qui pourrait ne pas être clair à 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 séquencé par rapport à 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 devraient-ils? Quelle utilité cela pourrait-il jamais avoir?) Donc, cela ipourrait encore être 5. Ou votre disque dur peut être vide. Ainsi, la réponse à votre question est:

Il s'agit d'un comportement indéfini car il n'est pas défini quel est le comportement.

(Cela mérite d'être souligné car de nombreux programmeurs pensent que "non défini" signifie "aléatoire" ou "imprévisible". Ce n'est pas le cas; cela signifie qu'il n'est pas défini par la norme. Le comportement pourrait être cohérent à 100% et toujours indéfini.)

Serait-ce un comportement défini? Oui. At-il été défini? Non. Par conséquent, il est "indéfini".

Cela dit, "non défini" ne signifie pas qu'un compilateur formatera votre disque dur ... cela signifie qu'il le pourrait et que ce serait toujours un compilateur conforme aux normes. De manière réaliste, je suis sûr que g ++, Clang et MSVC feront tous ce que vous attendiez. Ils n'auraient tout simplement pas "à le faire".


Une autre question pourrait se poser: pourquoi le comité des normes C ++ a-t-il choisi de ne pas séquencer cet effet secondaire? . Cette réponse portera sur l'histoire et les opinions du comité. Ou à quoi bon avoir cet effet secondaire non séquencé en C ++? , qui permet toute justification, qu'il s'agisse ou non du raisonnement réel du comité des normes. Vous pouvez poser ces questions ici ou sur programmers.stackexchange.com.

Paul Draper
la source
9
@hvd, oui, en fait je sais que si vous activez -Wsequence-pointpour g ++, il vous avertira.
Paul Draper
47
"Je suis sûr que g ++, Clang et MSVC feront tous ce que vous attendiez" Je ne ferais pas confiance à un compilateur moderne. Ils sont mauvais. Par exemple, ils pourraient reconnaître qu'il s'agit d'un comportement non défini et supposer que ce code est inaccessible. S'ils ne le font pas aujourd'hui, ils pourraient le faire demain. Tout UB est une bombe à retardement.
CodesInChaos
8
@BlacklightShining "votre réponse est mauvaise parce qu'elle n'est pas bonne" n'est pas une rétroaction très utile, n'est-ce pas?
Vincent van der Weele
13
@BobJarvis La compilation n'a absolument aucune obligation de générer du code correct même à distance face à un comportement non défini. Il peut même supposer que ce code n'est même jamais appelé et donc remplacer le tout par un nop (Notez que les compilateurs font en fait de telles hypothèses face à UB). Par conséquent, je dirais que la réaction correcte à un tel rapport de bogue ne peut être que "fermée, fonctionne comme prévu"
Grizzly
7
@SchighSchagh Parfois, une reformulation des termes (qui n'apparaît qu'en surface comme une réponse tautologique) est ce dont les gens ont besoin. La plupart des gens novices en matière de spécifications techniques pensent que les undefined behaviormoyens something random will happensont loin d'être le cas la plupart du temps.
Izkata
27

Une raison pratique de ne pas faire d'exception aux règles simplement 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);

Considérez le cas où cela a été autorisé.

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

 #define VALUEB 2

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

Conclusion: il n'y a pas d'exception à la règle car cela rendrait la compilation réussie dépendante de la valeur (plutôt du type) d'une constante.

ÉDITER

@HeartWare a souligné que les expressions constantes du formulaire A DIV Bne sont pas autorisées dans certaines langues, quand Best 0, et entraînent l'échec de la compilation. Par conséquent, le changement d'une constante peut entraîner des erreurs de compilation à un autre endroit. Ce qui est, à mon humble avis, malheureux. Mais il est certainement bon de restreindre ces choses à l'inévitable.

Ingo
la source
Bien sûr, mais l'exemple ne utiliser des entiers littéraux. Votre f(i = VALUEA, i = VALUEB);a certainement le potentiel d'un comportement indéfini. J'espère que vous ne codez pas vraiment contre les valeurs derrière les identifiants.
Wolf
3
@Wold Mais le compilateur ne voit pas les macros de préprocesseur. Et même si ce n'était pas le cas, il est difficile de trouver un exemple dans n'importe quel langage de programmation, où un code source se compile jusqu'à ce que l'on change une constante int de 1 à 2. C'est tout simplement inacceptable et inexplicable, alors que vous voyez de très bonnes explications ici pourquoi ce code est cassé même avec les mêmes valeurs.
Ingo
Oui, la compilation ne voit pas les macros. Mais était- ce là la question?
Wolf
1
Votre réponse est sans objet, lisez la réponse de Harmic et le commentaire du PO à ce sujet.
Wolf
1
Ça pourrait faire SomeProcedure(A, B, B DIV (2-A)). Quoi qu'il en soit, si le langage stipule que CONST doit être entièrement évalué au moment de la compilation, alors, bien sûr, ma réclamation n'est pas valable pour ce cas. Puisqu'il brouille en quelque sorte la distinction entre temps de compilation et temps d'exécution. Serait-il également remarqué si nous écrivons CONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y; ?? Ou les fonctions ne sont-elles pas autorisées?
Ingo
12

La confusion est que le stockage d'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 fonctionne est plus important que le compilateur dans ce cas. Par exemple, sur ARM où chaque instruction ne peut pas transporter une constante complète de 32 bits, le stockage d'un entier 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 registre 32 bits, i 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 l'optimiser, il peut entrelacer deux fois la même séquence, et vous ne savez pas quelle valeur sera écrite 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 gentil 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 de considérer que même une constante peut ne pas être aussi évidente qu'elle semble l'être.

davidf
la source
Je suppose que sur ARM, le compilateur chargera simplement la constante à partir d'une table. Ce que vous décrivez ressemble plus à MIPS.
ach
1
@AndreyChernyakhovskiy Yep, mais dans un cas où ce n'est pas simplement -1(que le compilateur a stocké quelque part), mais c'est plutôt 3^81 mod 2^32, mais constant, alors le compilateur peut faire exactement ce qui est fait ici, et dans un levier d'omtimisation, j'entrelace les séquences d'appels pour éviter d'attendre.
yo
@tohecz, oui, je l'ai déjà vérifié. En effet, le compilateur est trop intelligent pour charger chaque constante à partir d'une table. De toute façon, il n'utiliserait jamais le même registre pour calculer les deux constantes. Cela «dé-définirait» certainement le comportement défini.
ach
@AndreyChernyakhovskiy Mais vous n'êtes probablement pas "tous les programmeurs de compilateurs C ++ dans le monde". N'oubliez pas qu'il existe des machines avec 3 registres courts disponibles uniquement pour les calculs.
yo
@tohecz, considérons l'exemple f(i = A, j = B)iet jsont deux objets distincts. Cet exemple n'a pas d'UB. Une machine ayant 3 registres courts n'est pas une excuse pour que le compilateur mélange les deux valeurs de Aet Bdans le même registre (comme indiqué dans la réponse de @ davidf), car cela briserait la sémantique du programme.
ach
11

Le comportement est généralement spécifié comme non défini s'il existe une raison concevable pour laquelle un compilateur qui essayait d'être "utile" pourrait faire quelque chose qui provoquerait un comportement totalement inattendu.

Dans le cas où une variable est écrite plusieurs fois sans rien garantir que les écritures se produisent à des moments distincts, certains types de matériel peuvent permettre à plusieurs opérations de «stockage» d'être exécutées simultanément à différentes adresses à l'aide d'une mémoire à double port. Cependant, certaines mémoires à double port interdisent expressément le scénario où deux magasins atteignent la même adresse simultanément, que les valeurs écrites correspondent ou non. Si un compilateur pour une telle machine remarque deux tentatives non séquencées d'écriture de la même variable, il peut refuser de compiler ou s'assurer que les deux écritures ne peuvent pas être planifiées simultanément. Mais si l'un des accès ou les deux se fait via un pointeur ou une référence, le compilateur peut ne pas toujours être en mesure de dire si les deux écritures peuvent atteindre le même emplacement de stockage. Dans ce cas, il peut planifier les écritures simultanément, provoquant une interruption matérielle lors de 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 lors de l'utilisation de magasins de types suffisamment petits pour être traités atomiquement. Essayer de stocker deux valeurs différentes de manière non séquencée peut provoquer une bizarrerie si un compilateur n'en est pas conscient; par exemple, étant donné:

uint8_t v;  // Global

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

si le compilateur aligne l'appel à "moo" et peut dire qu'il ne modifie pas "v", il peut stocker un 5 à v, puis un 6 à * p, puis passer 5 à "zoo", puis passez le contenu de v au "zoo". Si "zoo" ne modifie pas "v", il ne devrait y avoir aucun moyen de transmettre des valeurs différentes aux deux appels, mais cela pourrait facilement se produire de toute façon. D'un autre côté, dans les cas où les deux magasins écriraient la même valeur, une telle bizarrerie ne pourrait pas se produire et il n'y aurait sur la plupart des plateformes aucune raison raisonnable pour qu'une implémentation fasse quelque chose de bizarre. Malheureusement, certains rédacteurs du compilateur n'ont besoin d'aucune excuse pour des comportements stupides au-delà de "parce que la norme le permet", donc même ces cas ne sont pas sûrs.

supercat
la source
9

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

Étant donné que l'expression est spécifiée comme ayant un comportement indéfini, un compilateur malicieusement conforme peut afficher une image inappropriée lorsque vous évaluez f(i = -1, i = -1)et abandonnez l'exécution - tout en étant considéré comme complètement correct. Heureusement, aucun compilateur à ma connaissance ne le fait.

Amadan
la source
8

Il me semble que la seule règle relative au séquencement de l'expression d'argument de fonction est ici:

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

Cela ne définit pas le séquencement entre les expressions d'argument, nous nous retrouvons donc dans ce cas:

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.

Dans la pratique, sur la plupart des compilateurs, l'exemple que vous avez cité fonctionnera correctement (par opposition à "l'effacement de votre disque dur" et d'autres conséquences théoriques non définies du comportement).
Il s'agit cependant d'un passif car il dépend du comportement spécifique du compilateur, même si les deux valeurs attribuées sont identiques. De plus, évidemment, si vous tentiez d'attribuer des valeurs différentes, les résultats seraient "vraiment" indéfinis:

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}
Martin J.
la source
8

C ++ 17 définit des règles d'évaluation plus strictes. En particulier, il séquence les 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 manière indéterminée lorsque A est séquencé avant B ou B est séquencé avant A , mais il n'est pas précisé lequel. [ Remarque : les évaluations séquencées de façon indéterminée ne peuvent pas se chevaucher, mais l'une ou l'autre pourrait être exécutée en premier. - note de fin ]

N5659 § 8.2.2:5
L'initialisation d'un paramètre, y compris chaque calcul de valeur associé et effet secondaire, est séquencée de façon indéterminée par rapport à celle de tout autre paramètre.

Il autorise certains cas qui seraient auparavant UB:

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
AlexD
la source
2
Merci d'avoir ajouté cette mise à jour pour c ++ 17 , donc je n'ai pas eu à le faire. ;)
Yakk - Adam Nevraumont
Génial, merci beaucoup pour cette réponse. Léger suivi: si fla signature était f(int a, int b), est-ce que C ++ 17 garantit cela a == -1et b == -2s'il est appelé comme dans le deuxième cas?
Nicu Stiurca
Oui. Si nous avons des paramètres aet b, alors i-then- aest initialisé à -1, puis i-then- best initialisé à -2, ou inversement . Dans les deux cas, on se retrouve avec a == -1et b == -2. C'est du moins ainsi que je lis " L'initialisation d'un paramètre, y compris chaque calcul de valeur associé et effet secondaire, est séquencée de façon indéterminée par rapport à celle de tout autre paramètre ".
AlexD
Je pense que c'est la même chose en C depuis toujours.
fuz
5

L'opérateur d'affectation peut être surchargé, auquel cas la commande peut être importante:

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
JohnB
la source
1
C'est vrai, mais la question portait sur les types scalaires , qui, selon d'autres, signifient essentiellement la famille int, la famille float et les pointeurs.
Nicu Stiurca
Le vrai problème dans ce cas est que l'opérateur d'affectation est avec état, donc même une manipulation régulière de la variable est sujette à des problèmes comme celui-ci.
AJMansfield
2

Ceci répond simplement au "Je ne sais pas ce que" objet scalaire "pourrait signifier en plus de quelque chose comme un int ou un flotteur".

J'interpréterais l '"objet scalaire" comme une abréviation de "objet de type scalaire", ou simplement "variable de type scalaire". Ensuite, pointer, enum(constante) sont de type scalaire.

Il s'agit d'un article MSDN de types scalaires .

Peng Zhang
la source
Cela ressemble un peu à une "réponse de lien uniquement". Pouvez-vous copier les bits pertinents de ce lien vers cette réponse (dans une citation)?
Cole Johnson
1
@ColeJohnson Ce n'est pas une réponse de lien seulement. Le lien est uniquement pour plus d'explications. Ma réponse est "pointeur", "énumération".
Peng Zhang
Je n'ai pas dit que votre réponse était une réponse en lien uniquement. J'ai dit qu'il "se lit comme [un]" . Je vous suggère de lire pourquoi nous ne voulons pas de réponses de lien uniquement dans la section d'aide. La raison en est que si Microsoft met à jour ses URL dans son site, ce lien se brise.
Cole Johnson
1

En fait, il y a une raison pour ne pas dépendre du fait que le compilateur vérifie ideux fois la même valeur, de sorte qu'il est possible de la remplacer par une seule affectation. Et si nous avons des 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)));
}
polkovnikov.ph
la source
1
Pas besoin de prouver le théorème de Fermat: attribuez simplement 1à i. Soit les deux arguments assignent 1et cela fait la "bonne" chose, soit les arguments assignent des valeurs différentes et c'est un comportement indéfini donc notre choix est toujours autorisé.