Comportement et points de séquence non définis

988

Que sont les «points de séquence»?

Quelle est la relation entre un comportement indéfini et des points de séquence?

J'utilise souvent des expressions amusantes et alambiquées comme a[++i] = i;pour me sentir mieux. Pourquoi devrais-je cesser de les utiliser?

Si vous avez lu ceci, assurez-vous de visiter la question de suivi Comportement non défini et points de séquence rechargés .

(Remarque: Ceci est censé être une entrée de la FAQ C ++ de Stack Overflow . Si vous voulez critiquer l'idée de fournir une FAQ sous cette forme, alors la publication sur la méta qui a commencé tout cela serait l'endroit pour le faire. Réponses à cette question est surveillée dans le salon de discussion C ++ , où l'idée de FAQ a commencé en premier lieu, donc votre réponse est très susceptible d'être lue par ceux qui ont eu l'idée.)

inconnu
la source

Réponses:

683

C ++ 98 et C ++ 03

Cette réponse concerne les anciennes versions de la norme C ++. Les versions C ++ 11 et C ++ 14 de la norme ne contiennent pas formellement de «points de séquence»; les opérations sont «séquencées avant» ou «non séquencées» ou «séquencées de façon indéterminée» à la place. L'effet net est essentiellement le même, mais la terminologie est différente.


Avertissement : D'accord. Cette réponse est un peu longue. Alors soyez patient en le lisant. Si vous connaissez déjà ces choses, les relire ne vous rendra pas fou.

Pré-requis : Une connaissance élémentaire du standard C ++


Que sont les points de séquence?

La norme dit

À certains points spécifiés de la séquence d'exécution appelés points de séquence , tous les effets secondaires des évaluations précédentes doivent être complets et aucun effet secondaire des évaluations suivantes ne doit s'être produit. (§1.9 / 7)

Effets secondaires? Quels sont les effets secondaires?

L'évaluation d'une expression produit quelque chose et si en plus il y a un changement dans l'état de l'environnement d'exécution, il est dit que l'expression (son évaluation) a un ou plusieurs effets secondaires.

Par exemple:

int x = y++; //where y is also an int

En plus de l'opération d'initialisation, la valeur de yest modifiée en raison de l'effet secondaire de l' ++opérateur.

Jusqu'ici tout va bien. Passons aux points de séquence. Une définition d'alternance de points seq donnée par l'auteur comp.lang.c Steve Summit:

Le point de séquence est un moment où la poussière s'est déposée et tous les effets secondaires qui ont été vus jusqu'à présent sont garantis d'être complets.


Quels sont les points de séquence courants répertoriés dans la norme C ++?

Ce sont:

  • à la fin de l'évaluation de l'expression complète ( §1.9/16) (Une expression complète est une expression qui n'est pas une sous-expression d'une autre expression.) 1

    Exemple :

    int a = 5; // ; is a sequence point here
  • dans l'évaluation de chacune des expressions suivantes après l'évaluation de la première expression ( §1.9/18) 2

    • a && b (§5.14)
    • a || b (§5.15)
    • a ? b : c (§5.16)
    • a , b (§5.18)(ici a, b est un opérateur virgule; in func(a,a++) ,n'est pas un opérateur virgule, c'est simplement un séparateur entre les arguments aet a++. Ainsi le comportement n'est pas défini dans ce cas (si aest considéré comme un type primitif))
  • lors d'un appel de fonction (que la fonction soit en ligne ou non), après l'évaluation de tous les arguments de fonction (le cas échéant) qui a lieu avant l'exécution de toute expression ou instruction dans le corps de fonction ( §1.9/17).

1: Remarque: l'évaluation d'une expression complète peut inclure l'évaluation de sous-expressions qui ne font pas lexiquement partie de l'expression complète. Par exemple, les sous-expressions impliquées dans l'évaluation des expressions d'argument par défaut (8.3.6) sont considérées comme étant créées dans l'expression qui appelle la fonction, pas l'expression qui définit l'argument par défaut

2: Les opérateurs indiqués sont les opérateurs intégrés, comme décrit dans l'article 5. Lorsque l'un de ces opérateurs est surchargé (article 13) dans un contexte valide, désignant ainsi une fonction d'opérateur définie par l'utilisateur, l'expression désigne une invocation de fonction et les opérandes forment une liste d'arguments, sans point de séquence implicite entre eux.


Qu'est-ce qu'un comportement indéfini?

La norme définit le comportement indéfini dans la section §1.3.12comme

comportement, tel qu'il pourrait survenir lors de l'utilisation d'une construction de programme erronée ou de données erronées, pour lequel la présente Norme internationale n'impose aucune exigence 3 .

Un comportement indéfini peut également être attendu lorsque la présente Norme internationale omet la description de toute définition explicite de comportement.

3: un comportement indéfini autorisé va de l'ignorance complète de la situation avec des résultats imprévisibles, au comportement pendant la traduction ou l'exécution du programme d'une manière documentée caractéristique de l'environnement (avec ou sans l'émission d'un message de diagnostic), jusqu'à la fin d'une traduction ou d'une exécution (avec émission d'un message de diagnostic).

En bref, un comportement indéfini signifie que tout peut arriver, des démons sortant du nez à la grossesse de votre petite amie.


Quelle est la relation entre le comportement indéfini et les points de séquence?

Avant d'entrer dans les détails, vous devez connaître la ou les différences entre le comportement non défini, le comportement non spécifié et le comportement défini par l'implémentation .

Vous devez également le savoir the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified.

Par exemple:

int x = 5, y = 6;

int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first.

Un autre exemple ici .


Maintenant, la norme en §5/4dit

  • 1) Entre le point de séquence précédent et suivant, un objet scalaire doit voir sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression.

Qu'est-ce que ça veut dire?

Informellement, cela signifie qu'entre deux points de séquence, une variable ne doit pas être modifiée plus d'une fois. Dans une instruction d'expression, l' next sequence pointest généralement au point-virgule de fin et l' previous sequence pointest à la fin de l'instruction précédente. Une expression peut également contenir un intermédiaire sequence points.

Dans la phrase ci-dessus, les expressions suivantes invoquent un comportement indéfini:

i++ * ++i;   // UB, i is modified more than once btw two SPs
i = ++i;     // UB, same as above
++i = 2;     // UB, same as above
i = ++i + 1; // UB, same as above
++++++i;     // UB, parsed as (++(++(++i)))

i = (i, ++i, ++i); // UB, there's no SP between `++i` (right most) and assignment to `i` (`i` is modified more than once btw two SPs)

Mais les expressions suivantes conviennent:

i = (i, ++i, 1) + 1; // well defined (AFAIK)
i = (++i, i++, i);   // well defined 
int j = i;
j = (++i, i++, j*i); // well defined

  • 2) En outre, la valeur antérieure ne doit être consultée que pour déterminer la valeur à stocker.

Qu'est-ce que ça veut dire? Cela signifie que si un objet est écrit dans une expression complète, tous les accès à celui-ci dans la même expression doivent être directement impliqués dans le calcul de la valeur à écrire .

Par exemple dans i = i + 1tous les accès de i(en LHS et en RHS) sont directement impliqués dans le calcul de la valeur à écrire. Donc ça va.

Cette règle contraint effectivement les expressions juridiques à celles dans lesquelles les accès précèdent manifestement la modification.

Exemple 1:

std::printf("%d %d", i,++i); // invokes Undefined Behaviour because of Rule no 2

Exemple 2:

a[i] = i++ // or a[++i] = i or a[i++] = ++i etc

est interdit car l'un des accès de i(celui en a[i]) n'a rien à voir avec la valeur qui finit par être stockée dans i (ce qui se produit en i++), et donc il n'y a pas de bon moyen de définir - que ce soit pour notre compréhension ou du compilateur - si l'accès doit avoir lieu avant ou après le stockage de la valeur incrémentée. Le comportement n'est donc pas défini.

Exemple 3:

int x = i + i++ ;// Similar to above

Suivez la réponse pour C ++ 11 ici .

Prasoon Saurav
la source
45
*p++ = 4 n'est pas un comportement indéfini. *p++est interprété comme *(p++). p++renvoie p(une copie) et la valeur stockée à l'adresse précédente. Pourquoi cela invoquerait-il UB? C'est parfaitement bien.
Prasoon Saurav
7
@Mike: AFAIK, il n'y a aucune copie (légale) de la norme C ++ à laquelle vous pourriez vous lier.
sbi
11
Eh bien, alors vous pourriez avoir un lien vers la page de commande pertinente de l'ISO. Quoi qu'il en soit, en y réfléchissant, l'expression "connaissance élémentaire du standard C ++" semble un peu contradictoire, car si vous lisez le standard, vous avez dépassé le niveau élémentaire. Peut-être pourrions-nous énumérer les éléments du langage dont vous avez besoin d'une compréhension de base, comme la syntaxe d'expression, l'ordre des opérations et peut-être la surcharge des opérateurs?
Mike DeSimone du
41
Je ne suis pas sûr que la norme soit la meilleure façon d'enseigner aux débutants
Inverse
6
@Adrian La première expression appelle un UB car il n'y a pas de point de séquence entre le dernier ++iet l'affectation à i. La deuxième expression n'invoque pas UB car l'expression ine modifie pas la valeur de i. Dans le deuxième exemple, le i++est suivi d'un point de séquence ( ,) avant l'appel de l'opérateur d'affectation.
Kolyunya
276

Ceci fait suite à ma réponse précédente et contient du matériel lié à C ++ 11. .


Pré-requis : Une connaissance élémentaire des relations (mathématiques).


Est-il vrai qu'il n'y a pas de points de séquence en C ++ 11?

Oui! C'est très vrai.

Les points de séquence ont été remplacés par des relations séquencées avant et séquencées après (et non séquencées et séquencées de façon indéterminée ) dans C ++ 11.


Qu'est-ce que cette chose «séquencée avant» exactement?

Séquencé avant (§1.9 / 13) est une relation qui est:

entre les évaluations exécutées par un seul thread et induit un ordre partiel strict 1

Formellement, cela signifie que deux évaluations ont été données (voir ci-dessous) A et B, si elles Asont séquencées auparavant B , l'exécution de A doit précéder l'exécution de B. Si An'est pas séquencé avant Bet Bn'est pas séquencé avant A, alors Aet ne Bsont pas séquencés 2 .

Les évaluations Aet Bsont séquencées de façon indéterminée lorsque l'une ou l'autre Aest séquencée avant Bou Best séquencée avant A, mais il n'est pas précisé lequel 3 .

[Notes]
1: Un ordre partiel strict est une relation binaire "<" sur un ensemble Pqui est asymmetric, et transitive, ce, pour tous a, bet cdans P, nous avons ce qui suit:
........ (i). si a <b alors ¬ (b <a) ( asymmetry);
........ (ii). si a <b et b <c alors a <c ( transitivity).
2: L'exécution des évaluations non séquencées peut se chevaucher .
3: 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.


Quelle est la signification du mot «évaluation» dans le contexte de C ++ 11?

En C ++ 11, l'évaluation d'une expression (ou d'une sous-expression) en général comprend:

  • calculs de valeurs (y compris la détermination de l'identité d'un objet pour l' évaluation de valeurs et la récupération d'une valeur précédemment affectée à un objet pour l' évaluation de valeurs ) et

  • déclenchement d' effets secondaires .

Maintenant (§1.9 / 14) dit:

Chaque calcul de valeur et effet secondaire associé à une expression complète est séquencé avant chaque calcul de valeur et effet secondaire associé à la prochaine expression complète à évaluer .

  • Exemple trivial:

    int x; x = 10; ++x;

    Le calcul de la valeur et l'effet secondaire associé à ++xest séquencé après le calcul de la valeur et l'effet secondaire dex = 10;


Il doit donc y avoir une relation entre le comportement indéfini et les choses mentionnées ci-dessus, non?

Oui! Droite.

Dans (§1.9 / 15), il a été mentionné que

Sauf indication contraire, les évaluations d'opérandes d'opérateurs individuels et de sous-expressions d'expressions individuelles ne sont pas séquencées 4 .

Par exemple :

int main()
{
     int num = 19 ;
     num = (num << 3) + (num >> 3);
} 
  1. L'évaluation des opérandes d' +opérateur n'est pas séquencée les unes par rapport aux autres.
  2. L'évaluation des opérandes <<et des >>opérateurs n'est pas séquencée les uns par rapport aux autres.

4: Dans une expression qui est évaluée plus d'une fois lors de l'exécution d'un programme, non séquencée et séquencés pour une période indéterminée évaluations de ses sous - expressions ne doivent pas être effectuées de manière cohérente dans différentes évaluations.

(§1.9 / 15) Les calculs de valeur des opérandes d'un opérateur sont séquencés avant le calcul de valeur du résultat de l'opérateur.

Cela signifie que dans x + yle calcul de la valeur de xet ysont séquencés avant le calcul de la valeur de (x + y).

Plus important

(§1.9 / 15) Si un effet secondaire sur un objet scalaire n'est pas séquencé par rapport à l'un ou l'autre

(a) un autre effet secondaire sur le même objet scalaire

ou

(b) un calcul de valeur utilisant la valeur du même objet scalaire.

le comportement n'est pas défini .

Exemples:

int i = 5, v[10] = { };
void  f(int,  int);
  1. i = i++ * ++i; // Undefined Behaviour
  2. i = ++i + i++; // Undefined Behaviour
  3. i = ++i + ++i; // Undefined Behaviour
  4. i = v[i++]; // Undefined Behaviour
  5. i = v[++i]: // Well-defined Behavior
  6. i = i++ + 1; // Undefined Behaviour
  7. i = ++i + 1; // Well-defined Behaviour
  8. ++++i; // Well-defined Behaviour
  9. f(i = -1, i = -1); // Undefined Behaviour (see below)

Lors de l'appel d'une fonction (que la fonction soit en ligne 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 du fonction appelée. [ Remarque: les calculs de valeur et les effets secondaires associés à différentes expressions d'argument ne sont pas séquencés . - note de fin ]

Expressions (5), (7)et (8)ne pas invoquer un comportement non défini. Consultez les réponses suivantes pour une explication plus détaillée.


Note finale :

Si vous trouvez un défaut dans le message, veuillez laisser un commentaire. Utilisateurs avec pouvoir (avec un représentant> 20000), n'hésitez pas à modifier le message pour corriger les fautes de frappe et autres erreurs.

Prasoon Saurav
la source
3
Au lieu de «asymétriques», les relations «antisymétriques» sont séquencées avant / après. Cela devrait être changé dans le texte pour se conformer à la définition d'un ordre partiel donnée plus loin (qui est également d'accord avec Wikipedia).
TemplateRex
1
Pourquoi 7) item dans le dernier exemple est-il un UB? Peut-être que ça devrait l'être f(i = -1, i = 1)?
Mikhail
1
J'ai corrigé la description de la relation "séquencé avant". Il s'agit d'un ordre partiel strict . De toute évidence, une expression ne peut pas être séquencée avant elle-même, donc la relation ne peut pas être réflexive. Par conséquent, il est asymétrique et non antisymétrique.
ThomasMcLeod
1
5) être bien confiné m'a époustouflé. l'explication de Johannes Schaub n'était pas tout à fait simple à obtenir. Surtout parce que je croyais que même en ++i(valeur évaluée avant l' +opérateur qui l'utilise), la norme ne dit toujours pas que son effet secondaire doit être terminé. Mais en fait, parce qu'elle renvoie une référence à une lvaluequi est ielle - même, elle DOIT avoir terminé l'effet secondaire puisque l'évaluation doit être terminée, donc la valeur doit être à jour. C'était la partie folle à obtenir en fait.
v.oddou
"Les membres du comité ISO C ++ pensaient que les trucs Sequence Points étaient assez difficiles à comprendre. Ils ont donc décidé de le remplacer par les relations mentionnées ci-dessus juste pour une formulation plus claire et une précision accrue." - avez-vous une référence pour cette réclamation? Il me semble que les nouvelles relations sont plus difficiles à comprendre.
MM
30

C ++ 17 ( N4659) inclut une proposition Affiner l'ordre d'évaluation des expressions pour le C ++ idiomatique qui définit un ordre plus strict d'évaluation des expressions.

En particulier, la phrase suivante

8.18 Opérateurs d'affectation et d'affectation composée :
....

Dans tous les cas, l'affectation est séquencée après le calcul de la valeur des opérandes droit et gauche et avant le calcul de la valeur de l'expression d'affectation. L'opérande droit est séquencé avant l'opérande gauche.

avec la clarification suivante

Une expression X est dite séquence avant une expression Y si chaque calcul de valeur et de tous les effets secondaires associés à l'expression X est séquencé avant chaque calcul de valeur et de tous les effets secondaires associés à l'expression Y .

rendre valides plusieurs cas de comportement non défini précédemment, y compris celui en question:

a[++i] = i;

Cependant, plusieurs autres cas similaires conduisent toujours à un comportement indéfini.

Dans N4140:

i = i++ + 1; // the behavior is undefined

Mais en N4659

i = i++ + 1; // the value of i is incremented
i = i++ + i; // the behavior is undefined

Bien sûr, l'utilisation d'un compilateur compatible C ++ 17 ne signifie pas nécessairement que l'on devrait commencer à écrire de telles expressions.

AlexD
la source
pourquoi le i = i++ + 1;comportement est-il défini dans c ++ 17, je pense que même si "l'opérande droit est séquencé avant l'opérande gauche", cependant la modification pour "i ++" et l'effet secondaire pour l'affectation ne sont pas séquencés, veuillez donner plus de détails pour les interpréter
jack X
@jackX J'ai étendu la réponse :).
AlexD
yup, je pense que le détail de l'interprétation de la phrase "L'opérande droit est séquencé avant l'opérande gauche" est plus utile. Tels que "L'opérande droit est séquencé avant l'opérande gauche" signifie que le calcul de la valeur et l'effet secondaire associé à l'opérande droit sont séquencé avant celui de l'opérande gauche. comme vous l'avez fait :-)
jack X
11

Je suppose qu'il y a une raison fondamentale pour le changement, ce n'est pas simplement cosmétique pour rendre l'ancienne interprétation plus claire: cette raison est la concurrence. L'ordre d'élaboration non spécifié est simplement la sélection de l'un des plusieurs ordres en série possibles, ce qui est très différent des ordres avant et après, car s'il n'y a pas d'ordre spécifié, une évaluation simultanée est possible: ce n'est pas le cas avec les anciennes règles. Par exemple dans:

f (a,b)

auparavant soit a puis b, soit, b alors a. Maintenant, a et b peuvent être évalués avec des instructions entrelacées ou même sur différents cœurs.

Yttrill
la source
5
Je crois, cependant, que si 'a' ou 'b' inclut un appel de fonction, ils sont séquencés de façon indéterminée plutôt que non séquencée, c'est-à-dire que tous les effets secondaires de l'un doivent se produire avant tout effet secondaire de la autre, bien que le compilateur n'ait pas besoin d'être cohérent sur lequel on va en premier. Si ce n'est plus vrai, cela briserait beaucoup de code qui repose sur le fait que les opérations ne se chevauchent pas (par exemple, si «a» et «b» établissent, utilisent et suppriment chacun un état statique partagé).
supercat
2

Dans C99(ISO/IEC 9899:TC3)ce qui semble absent de cette discussion jusqu'à présent, les participants suivants sont faits concernant l'ordre d'évaluation.

[...] l'ordre d'évaluation des sous-expressions et l'ordre dans lequel les effets secondaires se produisent ne sont pas spécifiés. (Section 6.5 pp 67)

L'ordre d'évaluation des opérandes n'est pas spécifié. Si une tentative est faite pour modifier le résultat d'un opérateur d'affectation ou pour y accéder après le point de séquence suivant, le comportement [sic] n'est pas défini. (Section 6.5.16 pp 91)

awiebe
la source
2
La question est étiquetée C ++ et non C, ce qui est bien parce que le comportement en C ++ 17 est assez différent du comportement des anciennes versions - et n'a aucun rapport avec le comportement en C11, C99, C90, etc. Ou porte très peu par rapport à elle. Dans l'ensemble, je suggère de supprimer cela. Plus important encore, nous devons trouver l'équivalent Q&A pour C et nous assurer qu'il est OK (et note que C ++ 17, en particulier, change les règles - le comportement en C ++ 11 et avant était plus ou moins le même que en C11, bien que le verbiage le décrivant en C utilise toujours des «points de séquence» alors que C ++ 11 et les versions ultérieures ne le font pas.
Jonathan Leffler