¿Cuáles son las garantías de orden de evaluación introducidas por C ++ 17?

95

¿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());
Johan Lundberg
fuente
Relacionado con Orden de evaluación de la instrucción de asignación en C ++ y ¿Este código de la sección 36.3.6 de la cuarta edición del “Lenguaje de programación C ++” tiene un comportamiento bien definido? que están ambos cubiertos por el papel. El primero podría ser un buen ejemplo adicional en su respuesta a continuación.
Shafik Yaghmour

Respuestas:

83

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 fse 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 fserá 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:

  1. ab
  2. a-> b
  3. a -> * b
  4. a (b1, b2, b3)
  5. b @ = a
  6. a [b]
  7. a << b
  8. 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, b3es todavía indeterminado. (gracias @KABoissonneault, todos los comentaristas).

Sin embargo, (como se señala @Yakk) y esto es importante: Incluso cuando b1, b2, b3son 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.

Johan Lundberg
fuente
1
"Una segunda propuesta subsidiaria reemplaza el orden de evaluación de las llamadas a funciones de la siguiente manera: la función se evalúa antes que todos sus argumentos, pero cualquier par de argumentos (de la lista de argumentos) tiene una secuencia indeterminada; lo que significa que uno se evalúa antes que el otro pero el no se especifica el orden, se garantiza que la función se evalúa antes que los argumentos. Esto refleja una sugerencia hecha por algunos miembros del Grupo de Trabajo Central ".
Yakk - Adam Nevraumont
1
Me da esa impresión del artículo que dice que "las siguientes expresiones se evalúan en el orden a, luego b, luego c, luego d" y luego se muestra a(b1, b2, b3), lo que sugiere que todas las bexpresiones no se evalúan necesariamente en ningún orden (de lo contrario, lo sería a(b, c, d))
KABoissonneault
1
@KABoissoneault, tiene razón y he actualizado la respuesta en consecuencia. Además, todas: las citas son de la versión 3, que es la versión votada hasta donde tengo entendido.
Johan Lundberg
2
@JohanLundberg Hay otra cosa del periódico que creo que es importante. 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.
Yakk - Adam Nevraumont
3
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_ptrconstructor relevante es explícito. Finalmente, x++ + ++xes indefinido, punto.
TC
44

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

  1. new A
  2. unique_ptr<A> constructor
  3. new B
  4. unique_ptr<B> constructor

El 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.

Barry
fuente
1
Un pequeño aparte, pero esta fue una de las razones para boost :: make_shared, y luego std :: make_shared (otra razón es menos asignaciones + mejor localidad). Parece que la motivación de excepción-seguridad / fuga de recursos ya no se aplica. Consulte el Ejemplo de código 3, boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/… Edite y stackoverflow.com/a/48844115 , plantsutter.com/2013/05/29/gotw-89-solution- punteros inteligentes
Max Barraclough
3
Me pregunto cómo afecta este cambio a la optimización. El compilador ahora ha reducido considerablemente el número de opciones sobre cómo combinar e intercalar instrucciones de CPU relacionadas con el cálculo de argumentos, por lo que puede conducir a una utilización más pobre de la CPU.
Jirafa violeta
2

Encontré algunas notas sobre el orden de evaluación de expresiones:

  • P rápida: ¿Por qué C ++ no tiene un orden específico para evaluar los argumentos de las funciones?

    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.

  • 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 se tratara de una llamada a función ( secuenciada indeterminadamente )

  • Expresiones ambiguas

    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>

lvccgd
fuente