c++ operator-overloading compiler-optimization evaluation order-of-evaluation

c++ - ¿Por qué se evalúa ''-++ a--+++b--'' en este orden?



operator-overloading compiler-optimization (5)

¿Por qué se imprime lo siguiente bD aD aB aA aC aU lugar de aD aB aA aC bD aU ? En otras palabras, ¿por qué se evalúa b-- antes de --++a--++ ?

#include <iostream> using namespace std; class A { char c_; public: A(char c) : c_(c) {} A& operator++() { cout << c_ << "A "; return *this; } A& operator++(int) { cout << c_ << "B "; return *this; } A& operator--() { cout << c_ << "C "; return *this; } A& operator--(int) { cout << c_ << "D "; return *this; } void operator+(A& b) { cout << c_ << "U "; } }; int main() { A a(''a''), b(''b''); --++a-- ++ +b--; // the culprit }

De lo que recojo, aquí es cómo el compilador analiza la expresión:

  • Tokenización del preprocesador: -- ++ a -- ++ + b -- ;
  • Prioridad del operador 1 : (--(++((a--)++))) + (b--) ;
  • + es asociativa de izquierda a derecha, pero no obstante, el compilador puede elegir evaluar primero la expresión de la derecha ( b-- ).

Supongo que el compilador elige hacerlo de esta manera porque conduce a un código mejor optimizado (menos instrucciones). Sin embargo, vale la pena señalar que obtengo el mismo resultado al compilar con /Od (MSVC) y -O0 (GCC). Esto me lleva a mi pregunta:

Desde que me preguntaron esto en una prueba que en principio debería ser de implementación / agnóstico del compilador, ¿hay algo en el estándar de C ++ que prescriba el comportamiento anterior, o es realmente no especificado? ¿Alguien puede citar un extracto de la norma que confirme? ¿Estaba mal tener tal pregunta en la prueba?

1 Me doy cuenta de que el compilador no sabe realmente acerca de la precedencia del operador o la asociatividad, sino que solo se preocupa por la gramática del lenguaje, pero esto debería mostrar el punto de vista de cualquier manera.


La declaracion de expresion

--++a-- ++ +b--; // the culprit

se puede representar de la siguiente manera

al principio como

( --++a-- ++ ) + ( b-- );

entonces como

( -- ( ++ ( ( a-- ) ++ ) ) ) + ( b-- );

y por fin como

a.operator --( 0 ).operator ++( 0 ).operator ++().operator --().operator + ( b.operator --( 0 ) );

Aquí hay un programa demostrativo.

#include <iostream> using namespace std; #include <iostream> using namespace std; class A { char c_; public: A(char c) : c_(c) {} A& operator++() { cout << c_ << "A "; return *this; } A& operator++(int) { cout << c_ << "B "; return *this; } A& operator--() { cout << c_ << "C "; return *this; } A& operator--(int) { cout << c_ << "D "; return *this; } void operator+(A& b) { cout << c_ << "U "; } }; int main() { A a(''a''), b(''b''); --++a-- ++ +b--; // the culprit std::cout << std::endl; a.operator --( 0 ).operator ++( 0 ).operator ++().operator --().operator + ( b.operator --( 0 ) ); return 0; }

Su salida es

bD aD aB aA aC aU bD aD aB aA aC aU

Puede imaginar la última expresión escrita en la forma funcional como una expresión postfix de la forma

postfix-expression ( expression-list )

donde la expresión postfix es

a.operator --( 0 ).operator ++( 0 ).operator ++().operator --().operator +

y la lista de expresiones es

b.operator --( 0 )

En el estándar de C ++ (5.2.2 llamada de función) se dice que

8 [Nota: Las evaluaciones de la expresión de fi jación del poste y de los argumentos no tienen ninguna secuencia entre sí. Todos los efectos secundarios de las evaluaciones de argumentos se secuencian antes de que se ingrese a la función (ver 1.9). "Nota final"

Por lo tanto, está definido por la implementación si al principio se evaluará el argumento o la expresión postfix. De acuerdo con la salida mostrada, el compilador primero evalúa el argumento y solo luego la expresión postfix.


Las reglas de precedencia y asociatividad del operador solo se utilizan para convertir su expresión de la notación original de "operadores en expresión" al formato equivalente de "llamada de función". Después de la conversión, terminas con un montón de llamadas de funciones anidadas, que se procesan de la manera habitual. En particular, el orden de evaluación de los parámetros no está especificado, lo que significa que no hay manera de decir qué operando de la llamada "binario +" se evaluará primero.

Además, tenga en cuenta que en su caso binary + se implementa como una función miembro, lo que crea cierta asimetría superficial entre sus argumentos: un argumento es un argumento "regular", otro es this . Tal vez algunos compiladores "prefieren" evaluar los argumentos "regulares" primero, que es lo que lleva a que b-- sea ​​evaluado primero en sus pruebas (podría terminar con un orden diferente del mismo compilador si implementa su binario + como una función independiente ). O tal vez no importa en absoluto.

Clang, por ejemplo, comienza con la evaluación del primer operando, dejando b-- para más adelante.


No hay algo en el estándar de C ++ que diga que las cosas deben ser evaluadas de esta manera. C ++ tiene el concepto de sequenced-before , donde se garantiza que algunas operaciones sucederán antes que otras operaciones. Este es un conjunto parcialmente ordenado; es decir, las operaciones de sosome se secuencian antes que otras, dos operaciones no se pueden secuenciar antes de las demás, y si a se secuencia antes de b, y b se secuencia antes de c, entonces a se secuencia antes de c. Sin embargo, hay muchos tipos de operaciones que no tienen garantías de secuencia previa. Antes de C ++ 11, en cambio había un concepto de un punto de secuencia, que no es exactamente el mismo sino similar.

Muy pocos operadores (solo && , ?: , Y || , creo) garantizan un punto de secuencia entre sus argumentos (e incluso entonces, hasta C ++ 17, esta garantía no existe cuando los operadores están sobrecargados). En particular, la adición no garantiza tal cosa. El compilador es libre de evaluar primero el lado izquierdo, evaluar primero el lado derecho o (creo) incluso evaluarlos simultáneamente.

A veces, cambiar las opciones de optimización puede cambiar los resultados o cambiar los compiladores. Aparentemente no estás viendo eso; No hay garantías aquí.


Tener en cuenta la prioridad de los operadores en c ++:

  1. a ++ a-- Incremento y decremento de sufijo / postfijo. De izquierda a derecha
  2. ++ a --a Incremento y decremento de prefijo. De derecha a izquierda
  3. a + b ab Suma y resta. De izquierda a derecha

Teniendo la lista en tu mente, puedes leer fácilmente la expresión incluso sin paréntesis:

--++a--+++b--;//will follow with --++a+++b--;//and so on --++a+b--; --++a+b; --a+b; a+b;

Y no se olvide de los operadores de prefijo y postfijo de diferencias esenciales en términos de evaluación de orden de variable y expresión))


Yo diría que se equivocaron al incluir esa pregunta.

Excepto como se indica, los siguientes extractos son todos de § [intro.execution] de N4618 (y no creo que nada de esto haya cambiado en los borradores más recientes).

El párrafo 16 tiene la definición básica de sequenced before , indeterminately sequenced , etc.

El párrafo 18 dice:

Excepto donde se indique, las evaluaciones de los operandos de los operadores individuales y de las subexpresiones de las expresiones individuales no tienen secuencia.

En este caso, estás (indirectamente) llamando a algunas funciones. Las reglas son bastante simples también:

Al llamar a una función (ya sea que la función esté o no en línea), cada cálculo de valor y efecto secundario asociado con cualquier expresión de argumento, o con la expresión de posfijo que designa la función llamada, se secuencia antes de la ejecución de cada expresión o declaración en el cuerpo del llamada funció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 se evalúa en el mismo hilo y como parte del mismo controlador de señales (si corresponde), A se secuencia antes que B o B se secuencia antes de A.

Poner eso en puntos de viñeta para indicar más directamente el orden:

  1. Primero evalúe los argumentos de la función, y lo que designe a la función que se llama.
  2. Evalúa el cuerpo de la propia función.
  3. Evaluar otra (sub-) expresión.

No se permite el intercalado a menos que algo inicie un hilo para permitir que otra cosa se ejecute en paralelo.

Entonces, ¿algo de este cambio antes de invocar las funciones a través de sobrecargas de operadores en lugar de directamente? El párrafo 19 dice "No":

Las restricciones de secuencia en la ejecución de la función llamada (como se describió anteriormente) son características de las llamadas de función evaluadas, independientemente de la sintaxis de la expresión que llame a la función.

§ [expr] / 2 también dice:

Los usos de los operadores sobrecargados se transforman en llamadas de función como se describe en 13.5. Los operadores sobrecargados obedecen las reglas de sintaxis y orden de evaluación especificadas en la Cláusula 5, pero los requisitos de tipo de operando y categoría de valor son reemplazados por las reglas para la llamada de función.

Operadores individuales

El único operador que ha utilizado que tiene requisitos un tanto inusuales con respecto a la secuenciación es el incremento posterior y el decremento posterior. Estos dicen (§ [expr.post.incr] / 1:

El cálculo del valor de la expresión ++ se secuencia antes de la modificación del objeto operando. Con respecto a una llamada de función de secuencia indeterminada, la operación de postfix ++ es una evaluación única. [Nota: Por lo tanto, una llamada a la función no debe intervenir entre la conversión de valor-a-valor y el efecto secundario asociado con cualquier operador único postfix ++. "Nota final"

Sin embargo, al final, esto es casi lo que probablemente esperaría: si pasa x++ como parámetro a una función, la función recibe el valor anterior de x , pero si x también está dentro del alcance dentro de la función, x tendrá el valor incrementado en el momento en que el cuerpo de la función comience a ejecutarse.

El operador + , sin embargo, no especifica el orden de la evaluación de sus operandos.

Resumen

El uso de operadores sobrecargados no impone ninguna secuencia en la evaluación de subexpresiones dentro de una expresión, más allá del hecho de que evaluar a un operador individual es una llamada de función, y tiene los requisitos de ordenamiento de cualquier otra llamada de función.

Más específicamente, en este caso, b-- es el operando de una llamada de función, y --++a-- ++ es la expresión que designa la función a la que se llama (o al menos el objeto sobre el que se llamará la función --the -- designa la función dentro de ese objeto). Como se indicó, el orden entre estos dos no se especifica (ni el operator + especifica un orden de evaluación de su operando izquierdo vs. derecho).