web-dev-qa-db-fra.com

Ce code de "The C ++ Programming Language" 4ème édition section 36.3.6 a-t-il un comportement bien défini?

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 ?

94
Shafik Yaghmour

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
_

Modifications de C++ 17

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]

104
Shafik Yaghmour

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 :

  • Les expressions de suffixe 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 inclut les affectations composées.
  • Les opérandes pour déplacer les opérateurs 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.1 et Clang 4.0.0.

4
ricky m