112 votes

Quelles sont les garanties d'ordre d'évaluation introduites par C++17 ?

Quelles sont les implications des garanties d'ordre d'évaluation C++17 votées dans P0145 sur le code C++ typique?

Qu'est-ce que cela change pour des choses comme le suivant?

i = 1;
f(i++, i)

et

std::cout << f() << f() << f();

ou

f(g(), h(), j());

0 votes

Lié à l'ordre d'évaluation de l'instruction d'assignation en C++ et Ce code du "Langage de programmation C++" 4ème édition section 36.3.6 a-t-il un comportement bien défini? qui sont tous deux traités dans le document. Le premier pourrait constituer un bel exemple supplémentaire dans votre réponse ci-dessous.

0 votes

0 votes

précédence de l'opérateur n'est pas pertinente.

108voto

Johan Lundberg Points 5835

Certains cas courants où l'ordre d'évaluation n'a jusqu'à présent pas été spécifié, sont spécifiés et valides avec C++17. Certains comportements indéfinis sont maintenant plutôt non spécifiés.

i = 1;
f(i++, i)

était indéfini, mais il est maintenant non spécifié. Plus précisément, ce qui n'est pas spécifié est l'ordre dans lequel chaque argument de f est évalué par rapport aux autres. i++ pourrait être évalué avant i, ou vice versa. En effet, il pourrait évaluer un deuxième appel dans un ordre différent, malgré le même compilateur.

Cependant, l'évaluation de chaque argument est nécessaire pour être exécutée complètement, avec tous les effets secondaires, avant l'exécution de tout autre argument. Ainsi, vous pouvez obtenir f(1, 1) (deuxième argument évalué en premier) ou f(1, 2) (premier argument évalué en premier). Mais vous n'obtiendrez jamais f(2, 2) ou quelque chose de similaire.

std::cout << f() << f() << f();

était non spécifié, mais il deviendra compatible avec la précédence des opérateurs de sorte que la première évaluation de f viendra en premier dans le flux (exemples ci-dessous).

f(g(), h(), j());

garde l'ordre non spécifié d'évaluation de g, h et j. Notez que pour getf()(g(),h(),j()), les règles indiquent que getf() sera évalué avant g, h, j.

Notez également l'exemple suivant tiré du texte de proposition :

 std::string s = "mais j'ai entendu dire que ça marche même si vous n'y croyez pas"
 s.replace(0, 4, "").replace(s.find("même"), 4, "seulement")
  .replace(s.find(" ne croyez"), 6, "");

L'exemple provient de Le langage de programmation C++, 4ème édition, Stroustrup, et c'était un comportement non spécifié, mais avec C++17, cela fonctionnera comme prévu. Il y avait des problèmes similaires avec les fonctions reprises (.then( . . . )).

Par exemple, considérez ce qui suit :

#include 
#include 
#include 
#include 

struct Orateur{
    int i =0;
    Orateur(std::vector mots) :mots(mots) {}
    std::vector mots;
    std::string operator()(){
        assert(mots.size()>0);
        if(i==mots.size()) i=0;
        // Version pré-C++17 :
        auto mot = mots[i] + (i+1==mots.size()?"\n":",");
        ++i;
        return mot;
        // Toujours pas possible avec C++17 :
        // return mots[i++] + (i==mots.size()?"\n":",");

    }
};

int main() {
    auto orateur = Orateur{{"Tout", "Travail", "et", "pas de", "jeu"}};
    std::cout << orateur() << orateur() << orateur() << orateur() << orateur() ;
}

Avec C++14 et antérieur, nous pouvons (et allons) obtenir des résultats tels que

jeu
pas,et,Travail,Tout,

au lieu de

Tout,travail,et,pas,jeu

Notez que ce qui précède équivaut en fait à

(((((std::cout << orateur()) << orateur()) << orateur()) << orateur()) << orateur()) ;

Mais encore, avant C++17, il n'y avait aucune garantie que les premiers appels viendraient d'abord dans le flux.

Références: D'après la proposition acceptée:

Les expressions postfixées sont évaluées de gauche à droite. Cela inclut les appels de fonctions et les expressions de sélection de membres.

Les expressions d'assignation sont évaluées de droite à gauche. Cela inclut les affectations composées.

Les opérandes des opérateurs de décalage sont évalués de gauche à droite. En résumé, les expressions suivantes sont évaluées dans l'ordre a, puis b, puis c, puis d:

  1. a.b
  2. a->b
  3. a->*b
  4. a(b1, b2, b3)
  5. b @= a
  6. a[b]
  7. a << b
  8. a >> b

De plus, nous suggérons la règle supplémentaire suivante: l'ordre d'évaluation d'une expression impliquant un opérateur surchargé est déterminé par l'ordre associé à l'opérateur natif correspondant, et non par les règles des appels de fonction.

Remarque de modification : Ma réponse originale a mal interprété a(b1, b2, b3). L'ordre de b1, b2, b3

Cependant, (comme le souligne @Yakk) et c'est important : Même lorsque b1, b2, b3 sont des expressions non triviales, chacune d'elles est complètement évaluée et liée au paramètre de la fonction respectivement avant que les autres ne commencent à être évaluées. La norme le stipule ainsi :

§5.2.2 - Appel de fonction 5.2.2.4 :

. . . L'expression postfixée est séquencée avant chaque expression dans la liste d'expressions et tout argument par défaut. Chaque calcul de valeur et effet secondaire associé à l'initialisation d'un paramètre, et la propagation elle-même, est séquencé avant chaque calcul de valeur et effet secondaire associé à l'initialisation de tout paramètre subsequent.

Cependant, une de ces nouvelles phrases manque dans le brouillon GitHub:

Chaque calcul de valeur et effet secondaire associé à l'initialisation d'un paramètre, et l'initialisation elle-même, est séquencé avant chaque calcul de valeur et effet secondaire associé à l'initialisation de tout paramètre ultérieur.

L'exemple y est là. Cela résout un problème ancien de décennies (comme expliqué par Herb Sutter) avec la sécurité des exceptions où des choses comme

f(std::unique_ptr a, std::unique_ptr b);

f(get_raw_a(), get_raw_a());

**

fuiraient si l'un des appels get_raw_a() lançait une exception avant l'autre pointeur brut était lié à son paramètre de pointeur intelligent.

Comme l'a souligné T.C., l'exemple est erroné car la construction de unique_ptr à partir d'un pointeur brut est explicite, empêchant cela de compiler.*


Remarquez également cette question classique (étiquetée C, pas C++) :

int x=0;
x++ + ++x;

est toujours indéfini.

**

0 votes

Certains éléments donnent l’impression que cela concerne un brouillon antérieur non accepté. Êtes-vous sûr de lire la version acceptée ?

1 votes

Une seconde proposition subsidiaire remplace l'ordre d'évaluation des appels de fonction comme suit : la fonction est évaluée avant tous ses arguments, mais toute paire d'arguments (de la liste d'arguments) est de séquence indéterminée ; cela signifie qu'un est évalué avant l'autre mais l'ordre n'est pas spécifié ; il est garanti que la fonction est évaluée avant les arguments. Cela reflète une suggestion faite par certains membres du groupe de travail principal.

0 votes

Autant que je sache, dans la proposition actuelle acceptée, seul l'exemple avec std::cout aura effectivement un ordre défini; les autres resteront indéfinis comme auparavant. Je veux dire, je pourrais me tromper, mais c'est ce que j'ai compris de la proposition qui a réellement abouti à C++17.

63voto

Barry Points 45207

L'entrelacement est interdit en C++17

En C++14, ce qui suit était non sécurisé :

void foo(std::unique_ptr, std::unique_ptr);

foo(std::unique_ptr(new A), std::unique_ptr(new B));

Il y a quatre opérations qui se produisent ici lors de l'appel de fonction


  • new A2. constructeur unique_ptr
  • new B
  • constructeur unique_ptr

**

L'ordre de ceux-ci était totalement non spécifié, donc un ordre parfaitement valide est (1), (3), (2), (4). Si cet ordre était choisi et que (3) lance une exception, alors la mémoire de (1) fuit - nous n'avons pas encore exécuté (2), qui aurait empêché la fuite.


En C++17, les nouvelles règles interdisent l'entrelacement. D'après [intro.execution]:

Pour chaque invocation de fonction F, pour chaque évaluation A qui se produit dans F et chaque évaluation B qui ne se produit pas dans F mais qui est évaluée sur le même thread et dans le cadre du même gestionnaire de signaux (le cas échéant), soit A est séquencée avant B, soit B est séquencée avant A.

Il y a une note de bas de page à cette phrase qui dit :

En d'autres termes, les exécutions de fonctions ne s'entrelacent pas entre elles.

Cela nous laisse avec deux ordres valides : (1), (2), (3), (4) ou (3), (4), (1), (2). Il n'est pas spécifié quel ordre est pris, mais les deux sont sûrs. Tous les ordres où (1) (3) se produisent avant (2) et (4) sont maintenant interdits.


1 votes

Un léger aparté, mais c'était l'une des raisons de boost::make_shared, et plus tard std::make_shared (l'autre raison étant moins d'allocations + une meilleure localisation). Il semble que la motivation pour la sûreté des exceptions et les fuites de ressources ne s'applique plus. Veuillez consulter l'exemple de code 3, boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/… Édition et stackoverflow.com/a/48844115, herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers

4 votes

Je me demande comment ce changement affecte l'optimisation. Le compilateur a maintenant un nombre considérablement réduit d'options quant à la manière de combiner et d'entrelacer les instructions CPU relatives au calcul des arguments, ce qui pourrait conduire à une moins bonne utilisation du CPU.

0 votes

Que se passe-t-il dans le cas de obj.modify().f(obj.access()): est-il bien défini si obj.modify() vient avant ou après obj.access()? (Il semble qu'au moins dans obj.modify().f(obj.access().foo()) tout obj.access().foo() se produirait "ensemble" plutôt que obj.modify() étant séquencé après obj.access() avant .foo().)

3voto

lvccgd Points 21

J'ai trouvé quelques notes sur l'ordre d'évaluation des expressions :

  • Question rapide : Pourquoi C++ n'a-t-il pas un ordre spécifié pour l'évaluation des arguments de fonction ?

    Quelques garanties d'ordre d'évaluation entourant les opérateurs surchargés et les règles de l'ensemble d'arguments complet ont été ajoutées en C++17. Mais il reste que l'ordre dans lequel chaque argument est évalué en premier est laissé non spécifié. En C++17, il est maintenant spécifié que l'expression indiquant quoi appeler (le code à gauche du ( de l'appel de fonction) va avant les arguments, et quel que soit l'argument qui est évalué en premier est évalué entièrement avant que le suivant ne soit démarré, et dans le cas d'une méthode d'objet la valeur de l'objet est évaluée avant les arguments de la méthode.

  • Ordre d'évaluation

    21) Chaque expression dans une liste de expressions séparées par des virgules dans un initialiseur parenthésé est évaluée comme pour un appel de fonction (indéterminément ordonnée)

  • Expressions ambiguës

    Le langage C++ ne garantit pas l'ordre dans lequel les arguments d'un appel de fonction sont évalués.

Dans P0145R3.Refining Expression Evaluation Order for Idiomatic C++ j'ai trouvé :

La valeur de calcul et l'effet secondaire associé de la postfix-expression sont ordonnés avant ceux des expressions dans la expression-list. Les initialisations des paramètres déclarés sont indéterminément ordonnées sans entrelacement.

Mais je ne l'ai pas trouvé dans la norme, à la place dans la norme j'ai trouvé :

6.8.1.8 Exécution séquentielle [intro.execution] Une expression X est dite être ordonnée avant une expression Y si chaque calcul de valeur et chaque effet secondaire associé à l'expression X est ordonné avant chaque calcul de valeur et chaque effet secondaire associé à l'expression Y.

6.8.1.9 Exécution séquentielle [intro.execution] Chaque calcul de valeur et effet secondaire associé à une expression complète est ordonné avant chaque calcul de valeur et effet secondaire associé à la prochaine expression complète à évaluer.

7.6.19.1 Opérateur de virgule [expr.comma] Une paire d'expressions séparées par une virgule est évaluée de gauche à droite; ...

Ainsi, j'ai comparé le comportement selon trois compilateurs pour les normes 14 et 17. Le code exploré est :

#include <iostream>

struct A
{
    A& addInt(int i)
    {
        std::cout << "add int: " << i << "\n";
        return *this;
    }

    A& addFloat(float i)
    {
        std::cout << "add float: " << i << "\n";
        return *this;
    }
};

int computeInt()
{
    std::cout << "compute int\n";
    return 0;
}

float computeFloat()
{
    std::cout << "compute float\n";
    return 1.0f;
}

void compute(float, int)
{
    std::cout << "compute\n";
}

int main()
{
    A a;
    a.addFloat(computeFloat()).addInt(computeInt());
    std::cout << "Function call:\n";
    compute(computeFloat(), computeInt());
}

Résultats (le plus cohérent est clang) :

  .tg {
    border-collapse: collapse;
    border-spacing: 0;
    border-color: #aaa;
  }

  .tg td {
    font-family: Arial, sans-serif;
    font-size: 14px;
    padding: 10px 5px;
    border-style: solid;
    border-width: 1px;
    overflow: hidden;
    word-break: normal;
    border-color: #aaa;
    color: #333;
    background-color: #fff;
  }

  .tg th {
    font-family: Arial, sans-serif;
    font-size: 14px;
    font-weight: normal;
    padding: 10px 5px;
    border-style: solid;
    border-width: 1px;
    overflow: hidden;
    word-break: normal;
    border-color: #aaa;
    color: #fff;
    background-color: #f38630;
  }

  .tg .tg-0pky {
    border-color: inherit;
    text-align: left;
    vertical-align: top
  }

  .tg .tg-fymr {
    font-weight: bold;
    border-color: inherit;
    text-align: left;
    vertical-align: top
  }

    C++14
    C++17

    gcc 9.0.1
    compute floatadd float: 1compute intadd int: 0Function call:compute intcompute floatcompute
    compute floatadd float: 1compute intadd int: 0Function call:compute intcompute floatcompute

    clang 9
    compute floatadd float: 1compute intadd int: 0Function call:compute floatcompute intcompute
    compute floatadd float: 1compute intadd int: 0Function call:compute floatcompute intcompute

    msvs 2017
    compute intcompute floatadd float: 1add int: 0Function call:compute intcompute floatcompute
    compute floatadd float: 1compute intadd int: 0Function call:compute intcompute floatcompute

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X