Ce code de la section 36.3.6 de la 4e édition de «The C ++ Programming Language» a-t-il un comportement bien défini?

94

Dans Bjarne Stroustrup's The C ++ Programming Language 4th edition section 36.3.6 STL-like Operations, 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 gcc(le voir en direct ) et Visual Studio(le voir en direct ), mais il n'échoue pas lors de l'utilisation de Clang (le voir en direct ).

Pourquoi ai-je des résultats différents? L'un de ces compilateurs évalue-t-il incorrectement l'expression de chaînage ou ce code présente-t-il une forme de comportement non spécifié ou non défini ?

Shafik Yaghmour
la source
Mieux:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
Ben Voigt
20
bogue mis à part, suis-je le seul à penser qu'un code laid comme celui-là ne devrait pas être dans le livre?
Karoly Horvath
5
@KarolyHorvath Notez que cout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c)est à peine moins moche.
Oktalist
1
@Oktalist: :) au moins j'en ai l'intention. il enseigne la recherche de noms dépendants des arguments et la syntaxe des opérateurs en même temps dans un format laconique ... et cela ne donne pas l'impression que vous devriez réellement écrire du code comme ça.
Karoly Horvath

Réponses:

104

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 replacedoit ê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 findappels pourraient être évalués avant ou après le replace, ce qui importe car il a un effet secondaire sur sd'une manière qui modifierait le résultat de find, il change la longueur de s. Donc, en fonction du moment où cela replaceest évalué par rapport aux deux findappels, 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 4et 7peut être divisé en plusieurs sous-expressions. Alors:

  • Aest séquencé avant Blequel est séquencé avant Clequel 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 4et 7par rapport à Bexplique la différence de résultats entre clanget gcclors de l'évaluation f2(). Dans mes tests, clangévalue Bavant d'évaluer 4et 7alors 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]

Shafik Yaghmour
la source
7
Je suis un peu surpris de voir que "de nombreux experts" ont négligé le problème, il est bien connu que l'évaluation de l' expression postfixe d'un appel de fonction n'est pas séquencée - avant d'évaluer les arguments (dans toutes les versions de C et C ++).
MM
@ShafikYaghmour Les appels de fonction sont séquencés de manière indéterminée les uns par rapport aux autres et à tout le reste, à l'exception des relations séquencées avant que vous avez notées. Toutefois, l' évaluation de 1, 2, 3, 5, 6, 8, 9, "even", "don't"et plusieurs instances de ssont non séquencée par rapport à l'autre.
TC
4
@TC non ce n'est pas le cas (c'est ainsi que ce "bug" survient). Par exemple foo().func( bar() ), il peut appeler foo()avant ou après l'appel bar(). L' expression de suffixe est foo().func. Les arguments et l'expression de suffixe sont séquencés avant le corps de func(), mais sans séquence l'un par rapport à l'autre.
MM
@MattMcNabb Ah, c'est vrai, j'ai mal lu. Vous parlez de l' expression postfix elle- même plutôt que de l'appel. Oui, c'est vrai, ils ne sont pas séquencés (à moins qu'une autre règle ne s'applique, bien sûr).
TC du
6
Il y a aussi le facteur que l'on a tendance à supposer que le code apparaissant dans un livre de B.Stroustrup est correct, sinon quelqu'un l'aurait sûrement déjà remarqué! (lié; les utilisateurs SO trouvent encore de nouvelles erreurs dans K&R)
MM
4

Ceci est destiné à ajouter des informations sur le sujet en ce qui concerne C ++ 17. La proposition ( Refining Expression Evaluation Order for Idiomatic C ++ Revision 2 ) pour C++17résoudre le problème citant le code ci-dessus était un spécimen.

Comme suggéré, j'ai ajouté des informations pertinentes de la proposition et de citer (met en évidence 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 touchent 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 attendu du programmeur. Il utilise le «chaînage» des appels de fonctions membres, une pratique standard courante. Ce code a été revu par des experts C ++ du 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.

Le document suggérait de changer la C++17règle sur l'ordre d'évaluation des expressions qui a été influencée par Cet qui existe depuis plus de trois décennies. Il a proposé que le langage garantisse des expressions idiomatiques contemporaines ou risque «des pièges et des sources de bogues obscurs et difficiles à trouver» , comme ce qui s'est passé avec le spécimen de code ci-dessus.

Il est proposé C++17d' exiger que chaque expression ait un ordre d'évaluation bien défini :

  • Les expressions Postfix sont évaluées de gauche à droite. Cela inclut les appels de fonctions et les expressions de sélection de membres.
  • Les expressions d'affectation sont évaluées de droite à gauche. Cela comprend les affectations composées.
  • Les opérandes des opérateurs de décalage sont évalués de gauche à droite.
  • L'ordre d'évaluation d'une expression impliquant un opérateur surchargé est déterminé par l'ordre associé à l'opérateur intégré correspondant, et non par les règles des appels de fonction.

Le code ci-dessus se compile avec succès en utilisant GCC 7.1.1et Clang 4.0.0.

Ricky m
la source