Dans Bjarne Stroustrup's The C++ Programming Language 4ème édition section 36.3.6
Opérations de type STL le code suivant est utilisé comme exemple de chaînage :
void f2()
{
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" ) ;
}
L'assertion échoue dans gcc
(voir en direct) et Visual Studio
(voir en direct), mais il n'échoue pas lors de l'utilisation Clang (voir en direct).
Pourquoi est-ce que j'obtiens des résultats différents? Certains de ces compilateurs évaluent-ils incorrectement l'expression de chaînage ou ce code présente-t-il une forme de non spécifié ou comportement non défini ?
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 car tous les effets secondaires se font dans des fonctions qui introduisent une relation de séquençage entre le côté effets dans ce cas.
Cet exemple est mentionné dans la proposition N4228: Ordre d'évaluation de l'expression des raffinements pour Idiomatic C++ qui dit ce qui suit au sujet du code dans la question:
[...] Ce code a été revu par des experts du C++ dans le monde entier et publié (The C++ Programming Language, 4e é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 la façon dont ce comportement interagit avec les appels de fonctions chaînés n'est pas aussi évidente. Ce n'était pas évident pour moi lorsque j'ai analysé ce cas pour la première fois et apparemment pas pour tous les experts critiques non plus.
À première vue, il peut apparaître que puisque chaque replace
doit être évalué de gauche à droite, les groupes d'arguments de fonction correspondants doivent également être évalués en tant que 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 sont uniquement séquencés avant par rapport à l'appel de fonction membre dont ils font partie. de. Cela affecte en particulier les appels suivants:
_s.find( "even" )
_
et:
_s.find( " don't" )
_
qui sont séquencés de façon indéterminée par rapport à
_s.replace(0, 4, "" )
_
les deux appels find
pourraient être évalués avant ou après le replace
, ce qui compte 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
. Ainsi, selon le moment où replace
sera évalué par rapport aux deux appels find
, 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
_ peuvent être encore décomposés en plusieurs sous-expressions. Alors:
A
est séquencé avant B
qui est séquencé avant C
qui est séquencé avant D
1
_ à _9
_ sont séquencés de façon indéterminée par rapport à d'autres sous-expressions avec certaines des exceptions énumérées ci-dessous 1
_ à _3
_ sont séquencés avant B
4
_ à _6
_ sont séquencés avant C
7
_ à _9
_ sont séquencés avant D
La clé de ce problème est que:
4
_ à _9
_ sont séquencés de façon indéterminée par rapport à B
L'ordre potentiel de choix de l'é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
_ tandis que gcc
l'évalue ensuite. Nous pouvons utiliser le programme de test suivant pour 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 que, sauf indication contraire, les évaluations des sous-expressions ne sont pas séquencées, cela provient de la ébauche de la norme C++ 11 section _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 séquencée avant de l'expression des appels de fonction et des 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é à 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. [...]
Nous savons également que l'accès des membres de la classe et donc le chaînage seront évalués de gauche à droite, à partir de la section _5.2.5
_ Accès des membres de la classe qui dit:
[...] L'expression du suffixe avant le point ou la flèche est évaluée;64 le résultat de cette évaluation, conjointement avec l'expression id, détermine le résultat de l'expression postfixe entière.
Remarque, dans le cas où id-expression finit par être une fonction membre non statique, il ne spécifie pas l'ordre d'évaluation de la expression-list dans le _()
_ car il s'agit d'une sous-expression distincte. La grammaire pertinente de _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
_
La proposition p0145r3: raffinement de l'ordre d'évaluation des expressions pour 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 postfix-expressions et leur expression-list.
[expr.call] p5 dit:
L'expression postfixe 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 chaque effet secondaire, est séquencée de façon 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 la saisie de la fonction (voir 4.6). —Fin 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 }
_—Fin exemple]
Ceci est destiné à ajouter des informations sur la question en ce qui concerne C++ 17. La proposition ( Ordonnance d'évaluation de l'expression des raffinements pour Idiomatic C++ Révision 2 ) pour C++17
a résolu le problème en citant le code ci-dessus à titre d'exemple.
Comme suggéré, j'ai ajouté des informations pertinentes de la proposition et de citer (souligne le mien):
L'ordre d'évaluation des expressions, tel qu'il est actuellement spécifié dans la norme, sape les conseils, les idiomes de programmation populaires ou la sécurité relative des installations de bibliothèque standard. Les pièges ne sont pas réservés aux novices ou aux programmeurs imprudents. Ils nous affectent tous sans discrimination, même lorsque nous connaissons les règles.
Considérez le fragment de programme suivant:
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"); }
L'assertion est censée valider le résultat prévu par le programmeur. Il utilise le "chaînage" des appels de fonction membre, une pratique standard courante. Ce code a été examiné par des experts du C++ dans le monde entier et publié (The C++ Programming Language, 4th edition.) Pourtant, sa vulnérabilité à un ordre d'évaluation non spécifié n'a été découvert que récemment par un outil.
Le document a suggéré de modifier la pré -C++17
règle sur l'ordre d'évaluation des expressions influencé par C
et existe depuis plus de trois décennies. Il a proposé que le langage devrait garantir les idiomes contemporains ou risquer "pièges et sources de bugs obscurs, difficiles à trouver" comme ce qui s'est passé avec le spécimen de code ci-dessus .
La proposition de C++17
doit exiger que chaque expression ait un ordre d'évaluation bien défini :
Le code ci-dessus se compile avec succès en utilisant GCC 7.1.1
et Clang 4.0.0
.