politica - ¿Cuáles son las garantías de orden de evaluación introducidas por C++ 17?
garantias constitucionales (3)
¿Cuáles son las implicaciones de las garantías de orden de evaluación votadas en C ++ 17 (P0145) en el código típico de C ++?
¿Qué cambia sobre cosas como
i=1;
f(i++, i)
y
std::cout << f() << f() << f() ;
o
f(g(),h(),j());
El intercalado está prohibido en C ++ 17
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
-
constructor
unique_ptr<A>
-
new B
-
unique_ptr<B>
El orden de estos no se especificó por completo, 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) tiene fugas; todavía no hemos ejecutado (2), lo que habría evitado la fuga.
En C ++ 17, las nuevas reglas prohíben el intercalado. De [intro.execution]:
Para cada invocación de función F, para cada evaluación A que se produce dentro de F y cada evaluación B que no se produce dentro de F pero se evalúa en el mismo subproceso y como parte del mismo controlador de señal (si corresponde), A se secuencia antes que B o B se secuencia antes de A.
Hay una nota al pie de 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 donde (1) (3) suceden antes (2) y (4) ahora están prohibidos.
Algunos casos comunes en los que el orden de evaluación hasta ahora
no se
ha
especificado
, se especifican y son válidos con
C++17
.
Algunos comportamientos indefinidos ahora no están especificados.
¿Qué pasa con cosas como
i=1; f(i++, i)
no estaba definido pero ahora no está especificado.
Específicamente, lo que no se especifica es el orden en que cada argumento de
f
se evalúa en relación con los demás.
i++
podría evaluarse antes que
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, se
requiere
que la evaluación de cada argumento se ejecute 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ás
f(2, 2)
ni nada de esa naturaleza.
std::cout << f() << f() << f() ;
No se especificó, pero se volverá compatible con la precedencia del operador para que la primera evaluación de
f
sea 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, j.
Tenga en cuenta que para
getf()(g(),h(),j())
, las reglas establecen que
getf()
se evaluará antes que
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 del
lenguaje de programación C ++, cuarta edición, 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 entrarían primero en la secuencia.
Referencias: De la propuesta aceptada :
Las expresiones de postfix 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 desplazar 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 operador incorporado correspondiente, no por las reglas para las llamadas a funciones.
Editar nota:
mi respuesta original malinterpretó
a(b1, b2, b3)
.
El orden de
b1
,
b2
,
b3
todavía no se ha especificado.
(Gracias @KABoissonneault, todos los comentaristas).
Sin embargo, (como señala @Yakk) y esto es importante: incluso cuando
b1
,
b2
,
b3
son expresiones no triviales, cada una de ellas se evalúa por completo
y se vincula al parámetro de función respectivo
antes de que las otras comiencen a evaluarse.
El estándar establece esto así:
§5.2.2 - Llamada de función 5.2.2.4:
. . . La expresión postfix 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í misma, se secuencian antes de cada cálculo de valor y efecto secundario asociado con la inicialización de cualquier parámetro posterior.
Sin embargo, una de estas nuevas oraciones falta 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í misma, se secuencian 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 explicó Herb Sutter ) con una seguridad excepcional 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 a
get_raw_a()
se lanzara antes de que el otro puntero sin procesar estuviera vinculado a su parámetro de puntero inteligente.
editar: como lo señaló TC, el ejemplo es defectuoso ya que la construcción unique_ptr desde el puntero sin procesar es explícita, lo que impide que se compile.
También tenga en cuenta esta question clásica (etiquetada C , no C ++ ):
int x=0; x++ + ++x;
Aún no está definido.
He encontrado algunas notas sobre el orden de evaluación de expresiones:
-
Quick Q: ¿Por qué c ++ no tiene un orden específico para evaluar argumentos de función?
Algún orden de evaluación garantiza los operadores sobrecargados y las reglas de argumento completo cuando se agregan en C ++ 17. Pero sigue siendo que qué argumento va primero se deja sin especificar. 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 que 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.
-
Orden de evaluación
21) Cada expresión en una lista de expresiones separadas por comas en un inicializador entre paréntesis se evalúa como si fuera una llamada de función ( secuenciada indeterminadamente )
-
Expresiones ambiguas
El lenguaje C ++ no garantiza el orden en que se evalúan los argumentos de una llamada de función.
En P0145R3. Refinando el orden de evaluación de expresión para Idiomatic C ++ he encontrado:
El cálculo del valor y el efecto secundario asociado de la expresión postfix se secuencian antes que los de las expresiones en la lista de expresiones. Las inicializaciones de los parámetros declarados se secuencian indeterminadamente sin intercalación.
Pero no lo encontré en estándar, en cambio en estándar he encontrado:
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úa de izquierda a derecha; ...
Entonces, comparé el comportamiento acorde en tres compiladores para 14 y 17 estándares. 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>