Le code présente un comportement non spécifié en raison d'un ordre d'évaluation non spécifié des sous-expressions bien qu'il n'invoque pas un comportement indéfini puisque tous les effets secondaires sont effectués dans des fonctions qui introduisent une relation de séquençage entre les effets secondaires dans ce cas.
Cet exemple est mentionné dans la proposition N4228: Refining Expression Evaluation Order for Idiomatic C ++ qui dit ce qui suit à propos du code dans la question:
[...] Ce code a été revu par des experts C ++ du monde entier et publié (The C ++ Programming Language, 4 e édition.) Pourtant, sa vulnérabilité à un ordre d'évaluation non spécifié n'a été découverte que récemment par un outil [.. .]
Détails
Il peut être évident pour beaucoup que les arguments des fonctions ont un ordre d'évaluation non spécifié, mais il n'est probablement pas aussi évident comment ce comportement interagit avec les appels de fonctions chaînées. Ce n'était pas évident pour moi lorsque j'ai analysé ce cas pour la première fois et apparemment pas non plus pour tous les examinateurs experts .
À première vue, il peut sembler que puisque chacun replace
doit être évalué de gauche à droite, les groupes d'arguments de fonction correspondants doivent également être évalués comme des groupes de gauche à droite.
Ceci est incorrect, les arguments de fonction ont un ordre d'évaluation non spécifié, bien que le chaînage des appels de fonction introduise un ordre d'évaluation de gauche à droite pour chaque appel de fonction, les arguments de chaque appel de fonction ne sont séquencés qu'avant par rapport à l'appel de fonction membre dont ils font partie de. En particulier, cela affecte les appels suivants:
s.find( "even" )
et:
s.find( " don't" )
qui sont séquencés de manière indéterminée par rapport à:
s.replace(0, 4, "" )
les deux find
appels pourraient être évalués avant ou après le replace
, ce qui importe car il a un effet secondaire sur s
d'une manière qui modifierait le résultat de find
, il change la longueur de s
. Donc, en fonction du moment où cela replace
est évalué par rapport aux deux find
appels, le résultat sera différent.
Si nous regardons l'expression de chaînage et examinons l'ordre d'évaluation de certaines des sous-expressions:
s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
^ ^ ^ ^ ^ ^ ^ ^ ^
A B | | | C | | |
1 2 3 4 5 6
et:
.replace( s.find( " don't" ), 6, "" );
^ ^ ^ ^
D | | |
7 8 9
Notez que nous ignorons le fait que 4
et 7
peut être divisé en plusieurs sous-expressions. Alors:
A
est séquencé avant B
lequel est séquencé avant C
lequel est séquencé avantD
1
à 9
être séquencés de manière indéterminée par rapport à d'autres sous-expressions avec certaines des exceptions énumérées ci-dessous
1
à 3
être séquencé avantB
4
à 6
être séquencé avantC
7
à 9
être séquencé avantD
La clé de ce problème est que:
4
à 9
être séquencés de manière indéterminée par rapport àB
L'ordre potentiel de choix d'évaluation pour 4
et 7
par rapport à B
explique la différence de résultats entre clang
et gcc
lors de l'évaluation f2()
. Dans mes tests, clang
évalue B
avant d'évaluer 4
et 7
alors que l' gcc
évalue après. Nous pouvons utiliser le programme de test suivant pour démontrer ce qui se passe dans chaque cas:
#include <iostream>
#include <string>
std::string::size_type my_find( std::string s, const char *cs )
{
std::string::size_type pos = s.find( cs ) ;
std::cout << "position " << cs << " found in complete expression: "
<< pos << std::endl ;
return pos ;
}
int main()
{
std::string s = "but I have heard it works even if you don't believe in it" ;
std::string copy_s = s ;
std::cout << "position of even before s.replace(0, 4, \"\" ): "
<< s.find( "even" ) << std::endl ;
std::cout << "position of don't before s.replace(0, 4, \"\" ): "
<< s.find( " don't" ) << std::endl << std::endl;
copy_s.replace(0, 4, "" ) ;
std::cout << "position of even after s.replace(0, 4, \"\" ): "
<< copy_s.find( "even" ) << std::endl ;
std::cout << "position of don't after s.replace(0, 4, \"\" ): "
<< copy_s.find( " don't" ) << std::endl << std::endl;
s.replace(0, 4, "" ).replace( my_find( s, "even" ) , 4, "only" )
.replace( my_find( s, " don't" ), 6, "" );
std::cout << "Result: " << s << std::endl ;
}
Résultat pour gcc
( voir en direct )
position of even before s.replace(0, 4, "" ): 26
position of don't before s.replace(0, 4, "" ): 37
position of even after s.replace(0, 4, "" ): 22
position of don't after s.replace(0, 4, "" ): 33
position don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it
Résultat pour clang
( voir en direct ):
position of even before s.replace(0, 4, "" ): 26
position of don't before s.replace(0, 4, "" ): 37
position of even after s.replace(0, 4, "" ): 22
position of don't after s.replace(0, 4, "" ): 33
position even found in complete expression: 22
position don't found in complete expression: 33
Result: I have heard it works only if you believe in it
Résultat pour Visual Studio
( voir en direct ):
position of even before s.replace(0, 4, "" ): 26
position of don't before s.replace(0, 4, "" ): 37
position of even after s.replace(0, 4, "" ): 22
position of don't after s.replace(0, 4, "" ): 33
position don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it
Détails de la norme
Nous savons qu'à moins d'être spécifiés, les évaluations des sous-expressions ne sont pas séquencées, cela provient du projet de section standard C ++ 11 1.9
Exécution du programme qui dit:
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. [...]
et nous savons qu'un appel de fonction introduit une relation avant séquencée des appels de fonction postfix expression et arguments par rapport au corps de la fonction, à partir de la section 1.9
:
[...] Lors de l'appel d'une fonction (que la fonction soit en ligne ou non), chaque calcul de valeur et effet secondaire associé à toute expression d'argument, ou à l'expression de suffixe 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. [...]
Nous savons également que l'accès aux membres de la classe et donc le chaînage seront évalués de gauche à droite, à partir de la section 5.2.5
Accès aux membres de la classe qui dit:
[...] L'expression de suffixe avant le point ou la flèche est évaluée; 64
le résultat de cette évaluation, avec l'expression id, détermine le résultat de l'expression postfix entière.
Notez que dans le cas où l' expression-id finit par être une fonction membre non statique, elle ne spécifie pas l'ordre d'évaluation de la liste d'expressions dans le ()
car il s'agit d'une sous-expression séparée. La grammaire pertinente des 5.2
expressions Postfix :
postfix-expression:
postfix-expression ( expression-listopt) // function call
postfix-expression . templateopt id-expression // Class member access, ends
// up as a postfix-expression
Changements C ++ 17
La proposition p0145r3: Refining Expression Evaluation Order for Idiomatic C ++ a apporté plusieurs modifications. Y compris les changements qui donnent au code un comportement bien spécifié en renforçant l'ordre des règles d'évaluation pour les expressions postfixes et leur liste d'expressions .
[expr.call] p5 dit:
L'expression de suffixe est séquencée avant chaque expression dans la liste d'expressions et tout argument par défaut . L'initialisation d'un paramètre, y compris chaque calcul de valeur associé et effet secondaire, est séquencée de manière indéterminée par rapport à celle de tout autre paramètre. [Remarque: Tous les effets secondaires des évaluations d'arguments sont séquencés avant que la fonction ne soit entrée (voir 4.6). —End note] [Exemple:
void f() {
std::string s = "but I have heard it works even if you don’t believe in it";
s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don’t"), 6, "");
assert(s == "I have heard it works only if you believe in it"); // OK
}
—End exemple]
s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );