¿Cuál es la respuesta correcta para cout << a ++ << a ;?

98

Recientemente en una entrevista hubo una siguiente pregunta de tipo objetivo.

int a = 0;
cout << a++ << a;

Respuestas:

a. 10
b. 01
c. comportamiento indefinido

Respondí la opción b, es decir, la salida sería "01".

Pero para mi sorpresa más tarde, un entrevistador me dijo que la respuesta correcta es la opción c: indefinida.

Ahora, conozco el concepto de puntos de secuencia en C ++. El comportamiento no está definido para la siguiente declaración:

int i = 0;
i += i++ + i++;

pero según mi entendimiento de la declaración cout << a++ << a, ostream.operator<<()se llamaría dos veces, primero con ostream.operator<<(a++)y después ostream.operator<<(a).

También verifiqué el resultado en el compilador VS2010 y su salida también es '01'.

pravs
fuente
30
¿Pediste una explicación? A menudo entrevisto a candidatos potenciales y estoy bastante interesado en recibir preguntas, muestra interés.
Brady
3
@jrok Es un comportamiento indefinido. Todo lo que haga la implementación (incluido enviar un correo electrónico insultante en su nombre a su jefe) es conforme.
James Kanze
2
Esta pregunta clama por una respuesta de C ++ 11 (la versión actual de C ++) que no menciona puntos de secuencia. Desafortunadamente, no estoy lo suficientemente informado sobre el reemplazo de puntos de secuencia en C ++ 11.
CB Bailey
3
Si no estuviera indefinido definitivamente no podría serlo 10, sería una 01o la otra 00. ( c++siempre evaluará el valor que ctenía antes de ser incrementado). E incluso si no estuviera indefinido, sería terriblemente confuso.
izquierda rotonda alrededor
2
Ya sabes, cuando leí el título “cout << c ++ << c”, pensé momentáneamente en él como una afirmación sobre la relación entre los lenguajes C y C ++, y alguna otra llamada “cout”. Ya sabes, como si alguien estuviera diciendo que pensaban que "cout" era muy inferior a C ++, y que C ++ era muy inferior a C, y probablemente por transitividad que "cout" era muy, muy inferior a C. :)
tchrist

Respuestas:

145

Tu puedes pensar en:

cout << a++ << a;

Como:

std::operator<<(std::operator<<(std::cout, a++), a);

C ++ garantiza que todos los efectos secundarios de evaluaciones anteriores se habrán realizado en puntos de secuencia . No hay puntos de secuencia entre la evaluación de argumentos de función, lo que significa que el argumento ase puede evaluar antes std::operator<<(std::cout, a++)o después del argumento . Entonces, el resultado de lo anterior no está definido.


Actualización de C ++ 17

En C ++ 17 se han actualizado las reglas. En particular:

En una expresión de operador de desplazamiento E1<<E2y E1>>E2, cada cálculo de valor y efecto secundario de E1se secuencia antes de cada cálculo de valor y efecto secundario de E2.

Lo que significa que requiere que el código produzca un resultado b, que genera 01.

Consulte P0145R3 Refinando el orden de evaluación de expresiones para Idiomatic C ++ para obtener más detalles.

Maxim Egorushkin
fuente
@Maxim: Gracias por la expansión. Con las llamadas que explicaste sería un comportamiento indefinido. Pero ahora, tengo una pregunta más (puede ser una tontería, y me falta algo básico y pienso en voz alta) ¿Cómo dedujo que la versión global de std :: operator << () se llamaría en lugar de ostream :: operator < <() versión de miembro. Al depurar, estoy aterrizando en una versión miembro de ostream :: operator << () llamada en lugar de la versión global y esa es la razón por la que inicialmente pensé que la respuesta sería 01
pravs
@Maxim No es que sea diferente, pero como ctiene tipo int, operator<<aquí están las funciones miembro.
James Kanze
2
@pravs: si operator<<es una función miembro o una función independiente, no afecta a los puntos de secuencia.
Maxim Egorushkin
11
El 'punto de secuencia' ya no se usa en el estándar C ++. Era impreciso y ha sido reemplazado por la relación "secuenciado antes / secuenciado después".
Rafał Dowgird
2
So the result of the above is undefined.Su explicación solo es buena para lo no especificado , no para lo indefinido . Sin embargo, James Kanze explicó cómo es más indefinido en su respuesta .
Desduplicador
68

Técnicamente, en general, esto es un comportamiento indefinido .

Pero hay dos aspectos importantes de la respuesta.

La declaración de código:

std::cout << a++ << a;

se evalúa como:

std::operator<<(std::operator<<(std::cout, a++), a);

El estándar no define el orden de evaluación de los argumentos de una función.
Entonces O bien:

  • std::operator<<(std::cout, a++) se evalúa primero o
  • ase evalúa primero o
  • podría ser cualquier orden definido por la implementación.

Este pedido no está especificado [Ref 1] según el estándar.

[Ref 1] C ++ 03 5.2.2 Llamada de función,
párrafo 8

El orden de evaluación de los argumentos no está especificado . Todos los efectos secundarios de las evaluaciones de expresiones de argumentos entran en vigor antes de que se introduzca la función. El orden de evaluación de la expresión de sufijo y la lista de expresiones de argumento no está especificado.

Además, no hay un punto de secuencia entre la evaluación de argumentos a una función, pero existe un punto de secuencia sólo después de la evaluación de todos los argumentos [Ref 2] .

[Ref 2] C ++ 03 1.9 Ejecución del programa [intro.execution]:
Para 17:

Cuando se llama a una función (esté o no en línea), hay un punto de secuencia después de la evaluación de todos los argumentos de la función (si los hay) que tiene lugar antes de la ejecución de cualquier expresión o declaración en el cuerpo de la función.

Tenga en cuenta que, aquí cse accede al valor de más de una vez sin un punto de secuencia intermedio, con respecto a esto, el estándar dice:

[Ref 3] C ++ 03 5 Expresiones [expr]:
Para 4:

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

El código se modifica cmás de una vez sin intervenir un punto de secuencia y no se accede a él para determinar el valor del objeto almacenado. Esta es una clara violación de la cláusula anterior y, por lo tanto, el resultado según lo ordena el estándar es un comportamiento indefinido [Ref 3] .

Alok Save
fuente
1
Técnicamente, el comportamiento no está definido, porque se modifica un objeto y se accede a él en otro lugar sin un punto de secuencia intermedio. Undefined no está sin especificar; deja aún más margen a la implementación.
James Kanze
1
@Als Sí. No había visto sus ediciones (aunque estaba reaccionando a la declaración de jrok de que el programa no puede hacer algo extraño, puede). Su versión editada es buena hasta donde llega, pero en mi opinión, la palabra clave es ordenamiento parcial ; los puntos de secuencia solo introducen un orden parcial.
James Kanze
1
@Als gracias por una descripción elaborada, realmente muy útil !!
pravs
4
El nuevo estándar C ++ 0x dice esencialmente lo mismo pero en diferentes secciones y en diferentes palabras :) Cita: (1.9 Ejecución de programa [intro.execution], par 15): "Si un efecto secundario en un objeto escalar no se secuencia en relación con ya sea otro efecto secundario sobre el mismo objeto escalar o un cálculo de valor utilizando el valor del mismo objeto escalar, el comportamiento no está definido ".
Rafał Dowgird
2
Creo que hay un error en esta respuesta. "std :: cout << c ++ << c;" no se puede traducir a "std :: operator << (std :: operator << (std :: cout, c ++), c)", porque std :: operator << (std :: ostream &, int) no existe. En cambio, se traduce como "std :: cout.operator << (c ++). Operator (c);", que en realidad tiene un punto de secuencia entre la evaluación de "c ++" y "c" (un operador sobrecargado se considera un llamada de función y por lo tanto hay un punto de secuencia cuando la llamada de función regresa). En consecuencia, se especifica el comportamiento y el orden de ejecución .
Christopher Smith
20

Los puntos de secuencia solo definen un orden parcial . En su caso, tiene (una vez que se realiza la resolución de sobrecarga):

std::cout.operator<<( a++ ).operator<<( a );

Hay un punto de secuencia entre a++y la primera llamada a std::ostream::operator<<, y hay un punto de secuencia entre la segunda ay la segunda llamada a std::ostream::operator<<, pero no hay un punto de secuencia entre a++y a; las únicas restricciones de orden son que a++se evalúen por completo (incluidos los efectos secundarios) antes de la primera llamada a operator<<, y que la segunda ase evalúe completamente antes de la segunda llamada a operator<<. (También existen restricciones de orden operator<<causales : la segunda llamada a no puede preceder a la primera, ya que requiere los resultados de la primera como argumento). §5 / 4 (C ++ 03) establece:

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.

Uno de los ordenamientos permisibles de su expresión es a++, ala primera llamada a operator<<la segunda llamada a operator<<; esto modifica el valor almacenado de a( a++) y accede a él de otra manera que para determinar el nuevo valor (el segundo a), el comportamiento no está definido.

James Kanze
fuente
Una captura de su cotización del estándar. El "excepto donde se indique", IIRC, incluye una excepción cuando se trata de un operador sobrecargado, que trata al operador como una función y, por lo tanto, crea un punto de secuencia entre la primera y la segunda llamada a std :: ostream :: operator << (int ). Por favor, corríjame si estoy equivocado.
Christopher Smith
@ChristopherSmith Un operador sobrecargado se comporta como una llamada de función. Si cfuera un tipo de usuario con un usuario definido ++, en lugar de int, los resultados no serían especificados, pero no habría un comportamiento indefinido.
James Kanze
1
@ChristopherSmith ¿Dónde se ve un punto de secuencia entre los dos cen foo(foo(bar(c)), c)? Hay un punto de secuencia cuando se llama a las funciones y cuando regresan, pero no se requiere una llamada de función entre las evaluaciones de los dos c.
James Kanze
1
@ChristopherSmith Si cfuera un UDT, los operadores sobrecargados serían llamadas a funciones e introducirían un punto de secuencia, por lo que el comportamiento no sería indefinido. Pero aún estaría sin especificar si la subexpresión cse evaluó antes o después c++, por lo que no se especificaría si obtuvo la versión incrementada o no (y en teoría, no tendría que ser el mismo cada vez).
James Kanze
1
@ChristopherSmith Todo antes del punto de secuencia ocurrirá antes que cualquier cosa después del punto de secuencia. Pero los puntos de secuencia solo definen un orden parcial. En la expresión en cuestión, por ejemplo, no hay un punto de secuencia entre las subexpresiones cy c++, por lo tanto, las dos pueden aparecer en cualquier orden. En cuanto al punto y coma ... Sólo provocan un punto de secuencia en la medida en que sean expresiones completas. Otros puntos de secuencia importantes son la llamada a la función: f(c++)verá el incremento cen f, y el operador de coma &&, ||y ?:también provocará puntos de secuencia.
James Kanze
4

La respuesta correcta es cuestionar la pregunta. La declaración es inaceptable porque el lector no puede ver una respuesta clara. Otra forma de verlo es que hemos introducido efectos secundarios (c ++) que hacen que la declaración sea mucho más difícil de interpretar. El código conciso es genial, siempre que su significado sea claro.

Paul Marrington
fuente
4
La pregunta puede exhibir una mala práctica de programación (e incluso C ++ inválido). Pero se supone que una respuesta responde a la pregunta que indica qué está mal y por qué está mal. Un comentario sobre la pregunta no es una respuesta, incluso si son perfectamente válidos. En el mejor de los casos, esto puede ser un comentario, no una respuesta.
PP