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

Resuelto Johan Lundberg asked hace 8 años • 3 respuestas

¿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 avatar Jul 21 '16 17:07 Johan Lundberg
Aceptado

Algunos casos comunes en los que el orden de evaluación no se ha especificado hasta el momento , 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 se evalúa cada argumento fen 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, es necesario que la evaluación de cada argumento se ejecute por completo, con todos los efectos secundarios, antes de la ejecución de cualquier otro argumento. Por lo tanto, podría obtener f(1, 1)(el segundo argumento se evalúa primero) o f(1, 2)(el primer argumento se evalúa primero). Pero nunca obtendrás f(2, 2)nada de esa naturaleza.

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

no se especificó, pero será compatible con la precedencia de operadores para que la primera evaluación de fsea 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.

Tenga en cuenta también 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 tener 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 lugar 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 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 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. un[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 operador incorporado correspondiente, no por las reglas para las llamadas a funciones.

Nota de edición: mi respuesta original se malinterpretó a(b1, b2, b3). El orden de b1, b2, b3aún no se ha especificado. (gracias @KABoissonneault, todos los comentaristas).

Sin embargo, (como señala @Yakk) y esto es importante: incluso cuando b1, b2, b3son expresiones no triviales, cada una de ellas se evalúa completamente y se vincula al parámetro de función respectivo antes de que las demás comiencen a evaluarse. La norma lo establece 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 de cualquier argumento predeterminado. Cada cálculo de valor y efecto secundario asociado con la inicialización de un parámetro, y la inicialización 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, falta una de estas nuevas frases 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 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 décadas ( como lo explica 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 get_raw_a()se realizara antes de que el otro puntero sin formato estuviera vinculado a su parámetro de puntero inteligente.

Como señaló TC, el ejemplo es defectuoso ya que la construcción Unique_ptr a partir del puntero sin formato es explícita, lo que impide la compilación.*

También tenga en cuenta esta pregunta clásica (etiquetada como C , no como C++ ):

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

aún no está definido.

Johan Lundberg avatar Jul 21 '2016 10:07 Johan Lundberg

El entrelazado 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 no se especificó en absoluto, por lo que un orden perfectamente válido es (1), (3), (2), (4). Si se seleccionó este orden y (3) arroja, entonces la memoria de (1) se pierde; 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.ejecución]:

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 que se evalúa en el mismo subproceso y como parte del mismo manejador de señales (si corresponde), A se secuencia antes de B. o B se secuencia antes que A.

Hay una nota a pie de página de esa frase 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é pedido se realiza, pero ambos son seguros. Todos los ordenamientos en los que (1) (3) ocurren antes de (2) y (4) ahora están prohibidos.

Barry avatar Sep 28 '2017 15:09 Barry

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

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

    En C++ 17 se agregaron algunas garantías de orden de evaluación en torno a operadores sobrecargados y reglas de argumentos completos. Pero sigue sin especificarse qué argumento va primero. En C++ 17, ahora se especifica que la expresión que indica qué llamar (el código a la izquierda de (de la llamada a la función) va antes de los argumentos, y el argumento que se evalúa primero se evalúa completamente antes de que se evalúe el siguiente. 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 a función ( con secuencia indeterminada )

  • 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.Refinamiento del orden de evaluación de expresiones para Idiomatic C++ encontré:

El cálculo del valor y el efecto secundario asociado de la expresión postfija se secuencian antes que los de las expresiones en la lista de expresiones. Las inicializaciones de los parámetros declarados están secuenciadas de forma indeterminada sin entrelazado.

Pero no lo encontré en estándar, sino que en estándar encontré:

6.8.1.8 Ejecución secuencial [intro.ejecución] Se dice que una expresión X está secuenciada 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 que se 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 correspondiente 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 (cuanto 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>
Expandir fragmento

lvccgd avatar Feb 07 '2019 08:02 lvccgd