Orden de ejecución de C ++ en el encadenamiento de métodos

108

El resultado de este programa:

#include <iostream> 
class c1
{   
  public:
    c1& meth1(int* ar) {
      std::cout << "method 1" << std::endl;
      *ar = 1;
      return *this;
    }
    void meth2(int ar)
    {
      std::cout << "method 2:"<< ar << std::endl;
    }
};

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu).meth2(nu);
}

Es:

method 1
method 2:0

¿Por qué nuno es 1 cuando meth2()comienza?

Moises Viñas
fuente
41
@MartinBonner: Aunque conozco la respuesta, no la llamaría "obvia" en ningún sentido de la palabra y, incluso si lo fuera, esa no sería una razón decente para votar en contra. ¡Decepcionante!
Lightness Races in Orbit
4
Esto es lo que obtiene cuando modifica sus argumentos. Las funciones que modifican sus argumentos son más difíciles de leer, sus efectos son inesperados para el próximo programador que trabaja en el código y dan lugar a sorpresas como esta. Sugiero encarecidamente que evite modificar cualquier parámetro excepto el invocante. Modificar el invocador no sería un problema aquí, porque el segundo método se llama sobre el resultado del primero, por lo que los efectos se ordenan en él. Sin embargo, todavía hay algunos casos en los que no lo serían.
Jan Hudec
@JanHudec Esta es precisamente la razón por la que la programación funcional pone tanto énfasis en la pureza de las funciones.
Pharap
2
Como ejemplo, una convención de llamadas basada en la pila probablemente preferiría empujar nu, &nuy cen la pila en ese orden, luego invocar meth1, empujar el resultado a la pila, luego invocar meth2, mientras que una convención de llamadas basada en registros querría cargar cy &nuen registros, invocar meth1, cargar nuen un registro, luego invocar meth2.
Neil

Respuestas:

66

Porque el orden de evaluación no está especificado.

Estás viendo nuen mainser evaluado 0antes incluso de meth1ser llamado. Este es el problema del encadenamiento. Aconsejo no hacerlo.

Simplemente cree un programa agradable, simple, claro, fácil de leer y de entender:

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}
Carreras de ligereza en órbita
fuente
14
Existe la posibilidad de que una propuesta para aclarar el orden de evaluación en algunos casos , que solucione este problema, llegue para C ++ 17
Revolver_Ocelot
7
Me gusta el encadenamiento de métodos (por ejemplo, <<para la salida y "constructores de objetos" para objetos complejos con demasiados argumentos para los constructores, pero se mezcla muy mal con los argumentos de salida.
Martin Bonner apoya a Monica
34
¿Entiendo esto bien? El orden de evaluación de meth1y meth2está definido, pero la evaluación del parámetro para meth2puede ocurrir antes de que meth1se llame ...?
Roddy
7
El encadenamiento de métodos está bien siempre que los métodos sean sensibles y solo modifiquen al invocador (para lo cual los efectos están bien ordenados, porque el segundo método se llama sobre el resultado del primero).
Jan Hudec
4
Es lógico, cuando lo piensas. Funciona comometh2(meth1(c, &nu), nu)
BartekChom
29

Creo que esta parte del borrador del estándar con respecto al orden de evaluación es relevante:

1.9 Ejecución del programa

...

  1. Excepto donde se indique, las evaluaciones de operandos de operadores individuales y de subexpresiones de expresiones individuales no están secuenciadas. Los cálculos de valor de los operandos de un operador se secuencian antes del cálculo de valor del resultado del operador. Si un efecto secundario en un objeto escalar no está secuenciado en relación con otro efecto secundario en el mismo objeto escalar o un cálculo de valor utilizando el valor del mismo objeto escalar, y no son potencialmente concurrentes, el comportamiento no está definido

y también:

5.2.2 Llamada a función

...

  1. [Nota: Las evaluaciones de la expresión de sufijo y de los argumentos no están secuenciadas entre sí. Todos los efectos secundarios de las evaluaciones de argumentos se secuencian antes de ingresar la función - nota final]

Entonces, para su línea c.meth1(&nu).meth2(nu);, considere lo que está sucediendo en el operador en términos del operador de llamada de función para la llamada final a meth2, de modo que veamos claramente el desglose en la expresión y el argumento de sufijo nu:

operator()(c.meth1(&nu).meth2, nu);

Las evaluaciones de la expresión de sufijo y el argumento para la llamada de función final (es decir, la expresión de sufijo c.meth1(&nu).meth2y nu) no están secuenciadas entre sí según la regla de llamada de función anterior. Por lo tanto, el efecto secundario del cálculo de la expresión de sufijo en el objeto escalar no arestá secuenciado en relación con la evaluación del argumento nuantes de la meth2llamada a la función. Según la regla de ejecución del programa anterior, este es un comportamiento indefinido.

En otras palabras, no es necesario que el compilador evalúe el nuargumento de la meth2llamada después de la meth1llamada; es libre de asumir que no hay efectos secundarios que meth1afecten la nuevaluación.

El código ensamblador producido por lo anterior contiene la siguiente secuencia en la mainfunción:

  1. La variable nuse asigna en la pila y se inicializa con 0.
  2. Un registro ( ebxen mi caso) recibe una copia del valor denu
  3. Las direcciones de nuy cse cargan en registros de parámetros
  4. meth1 se llama
  5. El registro del valor de retorno y el valor previamente almacenado en caché de nuen el ebxregistro se cargan en los registros de parámetros
  6. meth2 se llama

Críticamente, en el paso 5 anterior, el compilador permite que el valor en caché nudel paso 2 se reutilice en la llamada de función a meth2. Aquí se ignora la posibilidad de que nupueda haber sido cambiado por la llamada a meth1- 'comportamiento indefinido' en acción.

NOTA: Esta respuesta ha cambiado sustancialmente desde su forma original. Mi explicación inicial en términos de los efectos secundarios del cálculo de operandos que no se secuenciaron antes de la llamada final a la función fue incorrecta, porque lo son. El problema es el hecho de que el cálculo de los propios operandos tiene una secuencia indeterminada.

Smeeheey
fuente
2
Esto está mal. Las llamadas a funciones se secuencian indeterminadamente con otras evaluaciones en la función que llama (a menos que se imponga una restricción secuenciada antes); no se entrelazan.
TC
1
@TC: nunca dije nada sobre el intercalado de las llamadas a funciones. Solo me referí a los efectos secundarios de los operadores. Si observa el código ensamblador producido por lo anterior, verá que meth1se ejecuta antes meth2, pero el parámetro para meth2es un valor de nucaché en un registro antes de la llamada a meth1, es decir, el compilador ha ignorado los posibles efectos secundarios, que es coherente con mi respuesta.
Smeeheey
1
Usted está afirmando exactamente que - "no se garantiza que su efecto secundario (es decir, establecer el valor de ar) sea secuenciado antes de la llamada". La evaluación de la expresión-postfijo en una llamada de función (que es c.meth1(&nu).meth2) y la evaluación del argumento de esa llamada ( nu) generalmente no tienen secuencia, pero 1) sus efectos secundarios están todos secuenciados antes de la entrada meth2y 2) ya que c.meth1(&nu)es una llamada de función , tiene una secuencia indeterminada con la evaluación de nu. En el interior meth2, si de alguna manera obtiene un puntero a la variable en main, siempre verá 1.
TC
2
"Sin embargo, no se garantiza que el efecto secundario del cálculo de los operandos (es decir, establecer el valor de ar) sea secuenciado antes que nada (según 2) anterior)". Está absolutamente garantizado que se secuenciará antes de la llamada a meth2, como se indica en el elemento 3 de la página de referencia de cpp que está citando (que también se olvidó de citar correctamente).
TC
1
Tomó algo mal y lo empeoró. No hay absolutamente ningún comportamiento indefinido aquí. Siga leyendo [intro.execution] / 15, más allá del ejemplo.
TC
9

En el estándar C ++ de 1998, Sección 5, párrafo 4

Excepto donde se indique, el orden de evaluación de los operandos de operadores individuales y subexpresiones de expresiones individuales, y el orden en el que se producen los efectos secundarios, no se especifica. Entre el punto de secuencia anterior y siguiente, un objeto escalar tendrá su valor almacenado modificado como máximo una vez mediante la evaluación de una expresión. Además, solo se accederá al valor anterior para determinar el valor que se almacenará. Los requisitos de este párrafo deberán cumplirse para cada ordenamiento permitido de las subexpresiones de una expresión completa; de lo contrario, el comportamiento no está definido.

(He omitido una referencia a la nota al pie # 53 que no es relevante para esta pregunta).

Esencialmente, &nudebe evaluarse antes de llamar c1::meth1()y nudebe evaluarse antes de llamar c1::meth2(). Sin embargo, no hay ningún requisito que nudeba evaluarse antes &nu(por ejemplo, se permite que nuse evalúe primero, luego &nu, y luego c1::meth1()se llame, que podría ser lo que está haciendo su compilador). Por lo tanto, no se garantiza que la expresión *ar = 1in c1::meth1()se evalúe antes nude que main()se evalúe in para poder pasarla a c1::meth2().

Los estándares posteriores de C ++ (que actualmente no tengo en la PC que estoy usando esta noche) tienen esencialmente la misma cláusula.

Pedro
fuente
7

Creo que al compilar, antes de que las funciones meth1 y meth2 se llamen realmente, se les han pasado los parámetros. Me refiero a cuando usa "c.meth1 (& nu) .meth2 (nu);" el valor nu = 0 se ha pasado a meth2, por lo que no importa si "nu" se cambia más tarde.

puedes probar esto:

#include <iostream> 
class c1
{
public:
    c1& meth1(int* ar) {
        std::cout << "method 1" << std::endl;
        *ar = 1;
        return *this;
    }
    void meth2(int* ar)
    {
        std::cout << "method 2:" << *ar << std::endl;
    }
};

int main()
{
    c1 c;
    int nu = 0;
    c.meth1(&nu).meth2(&nu);
    getchar();
}

obtendrá la respuesta que desea

Camiseta saintor
fuente