Quelles sont les implications du vote en garanties de l'ordre d'évaluation C++ 17 (P0145) sur le code C++ typique?
Qu'est-ce que cela change sur des choses comme
i=1;
f(i++, i)
et
std::cout << f() << f() << f() ;
ou
f(g(),h(),j());
Certains cas courants où l'ordre d'évaluation a jusqu'à présent été non spécifié, sont spécifiés et valides avec C++17
. Certains comportements indéfinis sont désormais non spécifiés.
Qu'en est-il des choses comme
i=1; f(i++, i)
n'était pas défini mais n'est plus 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++
Peut être évalué avant i
, ou vice-versa. En effet, il pourrait évaluer un deuxième appel dans un ordre différent, bien qu'il soit sous le même compilateur.
Cependant, l'évaluation de chaque argument est obligatoire pour s'exécuter complètement, avec tous les effets secondaires, avant l'exécution de tout autre argument. Vous pouvez donc 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 quoi que ce soit d'autre de cette nature.
std::cout << f() << f() << f() ;
N'a pas été spécifié, mais deviendra compatible avec la priorité de l'opérateur afin que la première évaluation de f
apparaisse en premier dans le flux. (exemples ci-dessous).
f(g(),h(),j());
a toujours un ordre d'évaluation non spécifié de g, h, 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 du texte de la proposition:
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, "");
L'exemple vient de Le langage de programmation C++, 4ème édition, Stroustrup, et é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 pouvant être reprises (.then( . . . )
).
Comme autre exemple, considérez ce qui suit:
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
struct Speaker{
int i =0;
Speaker(std::vector<std::string> words) :words(words) {}
std::vector<std::string> words;
std::string operator()(){
assert(words.size()>0);
if(i==words.size()) i=0;
// pre- C++17 version:
auto Word = words[i] + (i+1==words.size()?"\n":",");
++i;
return Word;
// Still not possible with C++17:
// return words[i++] + (i==words.size()?"\n":",");
}
};
int main() {
auto spk = Speaker{{"All", "Work", "and", "no", "play"}};
std::cout << spk() << spk() << spk() << spk() << spk() ;
}
Avec C++ 14 et avant, nous pouvons (et obtiendrons) des résultats tels que
play
no,and,Work,All,
au lieu de
All,work,and,no,play
Notez que ce qui précède est en fait le même que
(((((std::cout << spk()) << spk()) << spk()) << spk()) << spk()) ;
Mais encore, avant C++ 17, il n'y avait aucune garantie que les premiers appels arriveraient en premier dans le flux.
Références: De la proposition acceptée :
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. En résumé, les expressions suivantes sont évaluées dans l'ordre a, puis b, puis c, puis d:
- un B
- a-> b
- a -> * b
- a(b1, b2, b3)
- b @ = a
- un B]
- a << b
- 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 intégré correspondant, et non par les règles des appels de fonction.
Modifier la note: Ma réponse originale a mal interprété a(b1, b2, b3)
. L'ordre de b1
, b2
, b3
N'est toujours pas spécifié. (merci @KABoissonneault, tous les commentateurs.)
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é au paramètre de fonction respectif avant que les autres ne commencent à être évalués. La norme stipule ceci comme ceci:
§5.2.2 - Appel de fonction 5.2.2.4:
. . . L'expression postfixe 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 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.
Cependant, une de ces nouvelles phrases manque dans le github draft :
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 est là. Il résout des problèmes vieux de plusieurs décennies ( comme expliqué par Herb Sutter ) avec une sécurité exceptionnelle où des choses comme
f(std::unique_ptr<A> a, std::unique_ptr<B> b);
f(get_raw_a(),get_raw_a());
fuirait si l'un des appels get_raw_a()
était lancé avant que l'autre pointeur brut ne soit lié à son paramètre de pointeur intelligent. modifier: comme l'a souligné T.C. l'exemple est défectueux car la construction unique_ptr à partir du pointeur brut est explicite, ce qui empêche sa compilation.
Notez également ce classique question (tagué [~ # ~] c [~ # ~], pas C++):
int x=0; x++ + ++x;
n'est pas encore défini.
En C++ 14, les éléments suivants n'étaient pas sûrs:
void foo(std::unique_ptr<A>, std::unique_ptr<B> );
foo(std::unique_ptr<A>(new A), std::unique_ptr<B>(new B));
Il y a quatre opérations qui se produisent ici pendant l'appel de fonction
new A
unique_ptr<A>
constructeurnew B
unique_ptr<B>
constructeurL'ordre de ceux-ci était complètement non spécifié, et donc un ordre parfaitement valide est (1), (3), (2), (4). Si cet ordre a été sélectionné et (3) jette, alors la mémoire de (1) fuit - nous n'avons pas encore exécuté (2), ce qui aurait empêché la fuite.
En C++ 17, les nouvelles règles interdisent l'entrelacement. Depuis [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 signal (le cas échéant), soit A est séquencé avant B ou B est séquencé avant A.
Il y a une note en bas de cette phrase qui se lit comme suit:
En d'autres termes, les exécutions de fonctions ne s'entrelacent pas.
Cela nous laisse avec deux commandes valides: (1), (2), (3), (4) ou (3), (4), (1), (2). Il n'est pas précisé quelle commande est prise, mais les deux sont sûrs. Toutes les commandes où (1) (3) se produisent avant (2) et (4) sont désormais interdites.
J'ai trouvé quelques notes sur l'ordre d'évaluation des expressions:
Un certain ordre d'évaluation garantit les opérateurs surchargés et les règles d'argument complet lorsqu'elles sont ajoutées en C++ 17. Mais il reste que l'argument qui va en premier n'est pas précisé. En C++ 17, il est maintenant spécifié que l'expression donnant quoi appeler (le code à gauche de (de l'appel de fonction) va avant les arguments, et quel que soit l'argument évalué en premier, il est évalué complètement avant le démarrage du suivant, 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.
21) Chaque expression dans une liste d'expressions séparées par des virgules dans un initialiseur entre parenthèses est évaluée comme si pour un appel de fonction ( séquencé de façon indéterminée )
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é:
Le calcul de la valeur et l'effet secondaire associé de l'expression postfixée sont séquencés avant ceux des expressions de la liste d'expressions. Les initialisations des paramètres déclarés sont séquencées de façon indéterminée sans entrelacement.
Mais je ne l'ai pas trouvé en standard, mais en standard j'ai trouvé:
6.8.1.8 Exécution séquentielle [intro.execution] Une expression X est dite séquencée avant une expression Y si chaque calcul de valeur et chaque effet secondaire associé à l'expression X est séquencé 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 séquencé avant chaque calcul de valeur et effet secondaire associé à la prochaine expression complète à évaluer.
7.6.19.1 Opérateur virgule [expr.comma] Une paire d'expressions séparées par une virgule est évaluée de gauche à droite; ...
J'ai donc 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 le bruit):
<style type="text/css">
.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
}
</style>
<table class="tg">
<tr>
<th class="tg-0pky"></th>
<th class="tg-fymr">C++14</th>
<th class="tg-fymr">C++17</th>
</tr>
<tr>
<td class="tg-fymr"><br>gcc 9.0.1<br></td>
<td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
<td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
</tr>
<tr>
<td class="tg-fymr">clang 9</td>
<td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
<td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
</tr>
<tr>
<td class="tg-fymr">msvs 2017</td>
<td class="tg-0pky">compute int<br>compute float<br>add float: 1<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
<td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
</tr>
</table>