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

14 minutos de lectura

¿Cuales son las garantias de orden de evaluacion introducidas por
Johan Lundberg

¿Cuáles son las implicaciones de la votada en Garantías de orden de evaluación C++17 (P0145) en el código típico de C++?

¿Qué cambia acerca de cosas como las siguientes?

i = 1;
f(i++, i)

y

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

o

f(g(), h(), j());

  • Relacionado con el 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 de “El lenguaje de programación C++” tiene un comportamiento bien definido? ambos cubiertos por el papel. El primero podría ser un buen ejemplo adicional en su respuesta a continuación.

    – Shafik Yaghmour

    15 mayo 2017 a las 17:57

  • También algo relacionado: orden de evaluación c ++ 17 con funciones de sobrecarga del operador.

    – dfrib

    29 de septiembre de 2017 a las 6:54

¿Cuales son las garantias de orden de evaluacion introducidas por
Johan Lundberg

Algunos casos comunes donde la orden de evaluación ha sido hasta ahora no especificadose 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 que cada argumento a f se evalúa en relación con los demás. i++ puede ser evaluado 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 es requerido para ejecutar completamente, con todos los efectos secundarios, antes de la ejecución de cualquier otro argumento. Entonces podrías obtener f(1, 1) (segundo argumento evaluado primero) o f(1, 2) (primer argumento evaluado primero). Pero nunca obtendrás f(2, 2) o cualquier otra cosa 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 f será el primero en la transmisión (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() será evaluado 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 viene de El lenguaje de programación C++, 4.ª edición, Stroustrup, y solía tener un comportamiento no especificado, pero con C++17 funcionará como se esperaba. Hubo problemas similares con 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 de que podamos (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 transmisión.

Referencias: Desde 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 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. abdominales
  2. a->b
  3. a->*b
  4. a(b1, b2, b3)
  5. b @ = un
  6. a[b]
  7. un << b
  8. un >> segundo

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 mal interpretada a(b1, b2, b3). El orden de b1, b2, b3 aún no se especifica. (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 completamente y vinculado al parámetro de la función respectiva antes de que se empiecen a evaluar los demás. La norma lo dice 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 propia inicialización, se secuencia 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 propia inicialización, se secuencia antes de cada cálculo de valor y efecto secundario asociado con la inicialización de cualquier parámetro posterior.

El ejemplo es allí. Resuelve problemas de hace décadas (como lo explica Herb Sutter) con 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() lanzaría antes de que el otro puntero sin formato se vinculara a su parámetro de puntero inteligente.

Como señaló TC, el ejemplo es defectuoso ya que la construcción unique_ptr desde el puntero sin formato es explícita, lo que impide que se compile.*

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

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

aún no está definido.

  • “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

    21 de julio de 2016 a las 12:05

  • Me da la impresión del artículo que dice que “las siguientes expresiones se evalúan en el orden aluego bluego cluego d” y luego mostrando a(b1, b2, b3)sugiriendo que todos b Las expresiones no se evalúan necesariamente en ningún orden (de lo contrario, sería a(b, c, d))

    – KABoissonneault

    21 de julio de 2016 a las 12:14


  • @KABoissoneault, tiene razón y actualicé la respuesta en consecuencia. Además, todo: las comillas están form versión 3, que es la versión votada según tengo entendido.

    – Johan Lundberg

    21 de julio de 2016 a las 12:28


  • @JohanLundberg Hay otra cosa del periódico que creo que es importante. a(b1()(), b2()()) puede pedir b1()() y b2()() en cualquier orden, pero no poder hacer b1() luego b2()() luego b1()(): ya no podrá intercalar sus ejecuciones. En resumen, “8. ORDEN ALTERNO DE EVALUACIÓN PARA LLAMADAS DE FUNCIÓN” fue parte del cambio aprobado.

    – Yakk – Adam Nevraumont

    21 de julio de 2016 a las 13:40


  • f(i++, i) estaba indefinido. Ahora es sin especificar. El ejemplo de cadena de Stroustrup probablemente no estaba especificado, no indefinido. ` f(get_raw_a(),get_raw_a());` no se compilará desde el unique_ptr constructor es explícito. Finalmente, x++ + ++x es indefinido, punto.

    – CT

    21 de julio de 2016 a las 17:26


¿Cuales son las garantias de orden de evaluacion introducidas por
barry

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 suceden aquí durante la llamada de 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 filtra; aún no hemos ejecutado (2), lo que habría evitado la fuga.


En C++17, las nuevas reglas prohíben el intercalado. Desde [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 que se evalúa en el mismo subproceso y como parte del mismo controlador de señal (si lo hay), A se secuencia antes que B o B se secuencia antes que A.

Hay una nota al pie de esa oración que dice:

En otras palabras, las ejecuciones de funciones no se intercalan entre sí.

Esto nos deja con dos órdenes válidos: (1), (2), (3), (4) o (3), (4), (1), (2). No se especifica qué pedido se toma, pero ambos son seguros. Todos los ordenamientos donde (1) (3) suceden antes de (2) y (4) ahora están prohibidos.

  • 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/… Editar y stackoverflow.com/a/48844115, herbsutter.com/2013/05/29/gotw-89-solution-smart-punteros

    –Max Barraclough

    6 de julio de 2018 a las 9:16


  • Me pregunto cómo afecta este cambio a la optimización. El compilador ahora tiene una cantidad muy reducida 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 de CPU más pobre.

    – Jirafa Violeta

    22 de agosto de 2019 a las 7:34

  • ¿Qué pasa con el caso de obj.modify().f(obj.access()): ¿Está bien definido si obj.modify() viene antes o después obj.access()? (Parece que al menos en obj.modify().f(obj.access().foo()) todo obj.access().foo() ocurriría “juntos” en lugar de obj.modify() siendo secuenciado después obj.access() antes de .foo().)

    – ben

    26 oct 2021 a las 18:09

  • @Ben Eso lo maneja la respuesta principal. En a.b, a se evalúa antes b.

    – Barry

    26/10/2021 a las 20:00

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 que rodean a los operadores sobrecargados y reglas de argumento completo. Pero queda sin especificar qué argumento va primero. 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 de función) va antes de los argumentos, y cualquier argumento que se evalúe primero se evalúa completamente antes de que comience el siguiente, 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 (secuencia indeterminada)

  • Expresiones ambiguas

    El lenguaje C++ no garantiza el orden en que se evalúan los argumentos de una llamada de función.

En P0145R3.Refinar orden de evaluación de expresiones para C++ idiomático He encontrado:

El cálculo del valor y el efecto secundario asociado de la expresión de sufijo se secuencian antes que los de las expresiones en la lista de expresiones. Las inicializaciones de los parámetros declarados son indeterminadamente secuenciado sin intercalado.

Pero no lo encontré en estándar, en cambio en estándar encontré:

6.8.1.8 Ejecución secuencial [intro.execution]

Se dice que una expresión X está secuenciada antes que una expresión Y si cada cálculo de valor y cada efecto secundario asociado con la expresión X está secuenciado 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 va a evaluar.

7.6.19.1 Operador coma [expr.comma]

Un par de expresiones separadas por una coma se evalúa 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 (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>

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad