¿Cuáles son las implicaciones de las garantías de orden de evaluación votadas en C ++ 17 (P0145) en el código C ++ típico?
¿Qué cambia en cosas como las siguientes?
i = 1;
f(i++, i)
y
std::cout << f() << f() << f();
o
f(g(), h(), j());
¿Cuáles son las implicaciones de las garantías de orden de evaluación votadas en C ++ 17 (P0145) en el código C ++ típico?
¿Qué cambia en cosas como las siguientes?
i = 1;
f(i++, i)
y
std::cout << f() << f() << f();
o
f(g(), h(), j());
Respuestas:
Algunos casos comunes en los que el orden de evaluación no se ha especificado hasta ahora se especifican y son válidos con C++17
. Algunos comportamientos indefinidos ahora no están especificados.
i = 1;
f(i++, i)
no estaba definido, pero ahora no está especificado. Específicamente, lo que no se especifica es el orden en el que f
se evalúa cada argumento en relación con los demás. i++
podría evaluarse antes i
, o viceversa. De hecho, podría evaluar una segunda llamada en un orden diferente, a pesar de estar bajo el mismo compilador.
Sin embargo, la evaluación de cada argumento debe ejecutarse completamente, con todos los efectos secundarios, antes de la ejecución de cualquier otro argumento. Por lo tanto, puede obtener f(1, 1)
(segundo argumento evaluado primero) o f(1, 2)
(primer argumento evaluado primero). Pero nunca obtendrá f(2, 2)
ni nada de esa naturaleza.
std::cout << f() << f() << f();
no se especificó, pero será compatible con la precedencia del operador, de modo que la primera evaluación de f
será la primera en la secuencia (ejemplos a continuación).
f(g(), h(), j());
todavía tiene un orden de evaluación no especificado de g, h y j. Tenga en cuenta que para getf()(g(),h(),j())
, las reglas establecen que getf()
se evaluará antes g, h, j
.
También tenga en cuenta el siguiente ejemplo del texto de la propuesta:
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, "");
El ejemplo proviene de The C ++ Programming Language , 4th edition, Stroustrup, y solía ser un comportamiento no especificado, pero con C ++ 17 funcionará como se esperaba. Hubo problemas similares con las funciones reanudables (.then( . . . )
).
Como otro ejemplo, considere lo siguiente:
#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() ;
}
Con C ++ 14 y antes, podemos (y obtendremos) resultados como
play
no,and,Work,All,
en vez de
All,work,and,no,play
Tenga en cuenta que lo anterior es en efecto lo mismo que
(((((std::cout << spk()) << spk()) << spk()) << spk()) << spk()) ;
Pero aún así, antes de C ++ 17 no había garantía de que las primeras llamadas llegaran primero a la secuencia.
Referencias: De la propuesta aceptada :
Las expresiones de sufijo se evalúan de izquierda a derecha. Esto incluye llamadas a funciones y expresiones de selección de miembros.
Las expresiones de asignación se evalúan de derecha a izquierda. Esto incluye asignaciones compuestas.
Los operandos para cambiar a los operadores se evalúan de izquierda a derecha. En resumen, las siguientes expresiones se evalúan en el orden a, luego b, luego c, luego d:
- ab
- a-> b
- a -> * b
- a (b1, b2, b3)
- b @ = a
- a [b]
- a << b
- a >> b
Además, sugerimos la siguiente regla adicional: el orden de evaluación de una expresión que involucra un operador sobrecargado está determinado por el orden asociado con el correspondiente operador incorporado, no las reglas para las llamadas a funciones.
Editar nota: Mi respuesta original fue malinterpretada a(b1, b2, b3)
. El orden de b1
, b2
, b3
es todavía indeterminado. (gracias @KABoissonneault, todos los comentaristas).
Sin embargo, (como se señala @Yakk) y esto es importante: Incluso cuando b1
, b2
, b3
son expresiones no triviales, cada uno de ellos están completamente evaluados y conectado con los parámetros de la función respectiva antes de que se comenzaron a ser evaluado los otros. El estándar establece esto así:
§5.2.2 - Llamada de función 5.2.2.4:
. . . La expresión-sufijo se secuencia antes de cada expresión en la lista de expresiones y cualquier argumento predeterminado. Cada cálculo de valor y efecto secundario asociado con la inicialización de un parámetro, y la inicialización en sí, se secuencia antes de cada cálculo de valor y efecto secundario asociado con la inicialización de cualquier parámetro posterior.
Sin embargo, falta una de estas nuevas oraciones en el borrador de GitHub :
Cada cálculo de valor y efecto secundario asociado con la inicialización de un parámetro, y la inicialización en sí, se secuencia antes de cada cálculo de valor y efecto secundario asociado con la inicialización de cualquier parámetro posterior.
El ejemplo está ahí. Resuelve problemas de hace décadas ( como lo explicó Herb Sutter ) con una excepción de seguridad donde cosas como
f(std::unique_ptr<A> a, std::unique_ptr<B> b);
f(get_raw_a(), get_raw_a());
se filtraría si una de las llamadas get_raw_a()
se lanzara antes de que el otro puntero sin formato estuviera vinculado a su parámetro de puntero inteligente.
Como lo señaló TC, el ejemplo es defectuoso ya que la construcción de unique_ptr a partir del puntero sin formato es explícita, lo que evita que se compile. *
También tenga en cuenta esta pregunta clásica (etiquetada C , no C ++ ):
int x=0;
x++ + ++x;
aún no está definido.
a
, luego b
, luego c
, luego d
" y luego se muestra a(b1, b2, b3)
, lo que sugiere que todas las b
expresiones no se evalúan necesariamente en ningún orden (de lo contrario, lo sería a(b, c, d)
)
a(b1()(), b2()())
puede ordenar b1()()
y b2()()
en cualquier orden, pero no puede hacerlo b1()
a continuación, b2()()
entonces b1()()
: ya no entrelazado de sus ejecuciones que pase. En resumen, "8. ORDEN DE EVALUACIÓN ALTERNATIVA PARA LAS LLAMADAS DE FUNCIÓN" fue parte del cambio aprobado.
f(i++, i)
estaba indefinido. Ahora no está especificado. El ejemplo de cadena de Stroustrup probablemente no estaba especificado, no estaba indefinido. `f (get_raw_a (), get_raw_a ());` no compilará ya que el unique_ptr
constructor relevante es explícito. Finalmente, x++ + ++x
es indefinido, punto.
En C ++ 14, lo siguiente no era seguro:
void foo(std::unique_ptr<A>, std::unique_ptr<B>);
foo(std::unique_ptr<A>(new A), std::unique_ptr<B>(new B));
Hay cuatro operaciones que ocurren aquí durante la llamada a la función
new A
unique_ptr<A>
constructornew B
unique_ptr<B>
constructorEl orden de estos fue completamente indeterminado, por lo que un orden perfectamente válido es (1), (3), (2), (4). Si se seleccionó este orden y (3) se arroja, entonces la memoria de (1) se filtra; aún no hemos ejecutado (2), lo que habría evitado la fuga.
En C ++ 17, las nuevas reglas prohíben el entrelazado. De [intro.execution]:
Para cada invocación de función F, para cada evaluación A que ocurre dentro de F y cada evaluación B que no ocurre dentro de F pero se evalúa en el mismo hilo y como parte del mismo manejador de señales (si lo hay), A se secuencia antes que B o B se secuencia antes de A.
Hay una nota a pie de página en esa oración que dice:
En otras palabras, las ejecuciones de funciones no se entrelazan entre sí.
Esto nos deja con dos ordenamientos válidos: (1), (2), (3), (4) o (3), (4), (1), (2). No se especifica qué orden se toma, pero ambos son seguros. Todos los pedidos en los que (1) (3) ocurren antes de (2) y (4) ahora están prohibidos.
Encontré algunas notas sobre el orden de evaluación de expresiones:
Algún orden de evaluación garantiza los operadores sobrecargados y las reglas de argumentos completos que se agregaron en C ++ 17. Pero sigue siendo que qué argumento va primero no se especifica. En C ++ 17, ahora se especifica que la expresión que da a qué llamar (el código a la izquierda de (de la llamada a la función) va antes de los argumentos, y cualquier argumento que se evalúe primero se evalúa completamente antes de que el siguiente sea iniciado, y en el caso de un método de objeto, el valor del objeto se evalúa antes que los argumentos del método.
21) Cada expresión en una lista de expresiones separadas por comas en un inicializador entre paréntesis se evalúa como si se tratara de una llamada a función ( secuenciada indeterminadamente )
El lenguaje C ++ no garantiza el orden en el que se evalúan los argumentos de una llamada de función.
En P0145R3.Refining Expression Evaluation Order for Idiomatic C ++ he encontrado:
El cálculo del valor y el efecto secundario asociado de la expresión-sufijo se secuencian antes que los de las expresiones en la lista de expresiones. Las inicializaciones de los parámetros declarados se secuencian indeterminadamente sin entrelazado.
Pero no lo encontré en estándar, sino en estándar que encontré:
6.8.1.8 Ejecución secuencial [intro.execution] Se dice que una expresión X se secuencia antes de una expresión Y si cada cálculo de valor y cada efecto secundario asociado con la expresión X se secuencia antes de cada cálculo de valor y cada efecto secundario asociado con la expresión Y .
6.8.1.9 Ejecución secuencial [intro.execution] Cada cálculo de valor y efecto secundario asociado con una expresión completa se secuencia antes de cada cálculo de valor y efecto secundario asociado con la siguiente expresión completa a evaluar.
7.6.19.1 Operador de coma [expr.comma] Un par de expresiones separadas por una coma se evalúan de izquierda a derecha; ...
Entonces, comparé el comportamiento de acuerdo en tres compiladores para los estándares 14 y 17. El código explorado es:
#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());
}
Resultados (el más consistente es el sonido metálico):
<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>