Comportamiento indefinido y puntos de secuencia

987

¿Qué son los "puntos de secuencia"?

¿Cuál es la relación entre el comportamiento indefinido y los puntos de secuencia?

A menudo uso expresiones divertidas y complicadas como a[++i] = i;, para hacerme sentir mejor. ¿Por qué debería dejar de usarlos?

Si ha leído esto, asegúrese de visitar la pregunta de seguimiento Comportamiento indefinido y puntos de secuencia recargados .

(Nota: Esto está destinado a ser una entrada a las preguntas frecuentes de C ++ de Stack Overflow . Si desea criticar la idea de proporcionar una pregunta frecuente en este formulario, entonces la publicación en meta que comenzó todo esto sería el lugar para hacerlo. Respuestas a esa pregunta se monitorea en la sala de chat de C ++ , donde la idea de preguntas frecuentes comenzó en primer lugar, por lo que es muy probable que su respuesta sea leída por aquellos a quienes se les ocurrió la idea).

desconocido
fuente

Respuestas:

683

C ++ 98 y C ++ 03

Esta respuesta es para las versiones anteriores del estándar C ++. Las versiones C ++ 11 y C ++ 14 del estándar no contienen formalmente 'puntos de secuencia'; las operaciones son 'secuenciadas antes' o 'no secuenciadas' o 'secuenciadas indeterminadamente' en su lugar. El efecto neto es esencialmente el mismo, pero la terminología es diferente.


Descargo de responsabilidad : está bien. Esta respuesta es un poco larga. Así que ten paciencia mientras lo lees. Si ya sabes estas cosas, leerlas nuevamente no te volverá loco.

Prerrequisitos : un conocimiento elemental de C ++ Standard


¿Qué son los puntos de secuencia?

El estándar dice

En ciertos puntos especificados en la secuencia de ejecución llamados puntos de secuencia , todos los efectos secundarios de las evaluaciones anteriores deberán estar completos y no se habrá producido ningún efecto secundario de las evaluaciones posteriores. (§1.9 / 7)

¿Efectos secundarios? ¿Qué son los efectos secundarios?

La evaluación de una expresión produce algo y, además, si hay un cambio en el estado del entorno de ejecución, se dice que la expresión (su evaluación) tiene algunos efectos secundarios.

Por ejemplo:

int x = y++; //where y is also an int

Además de la operación de inicialización, el valor de yse cambia debido al efecto secundario del ++operador.

Hasta aquí todo bien. Pasando a los puntos de secuencia. Una definición alternativa de puntos seq dada por el autor comp.lang.c Steve Summit:

El punto de secuencia es un punto en el tiempo en el que el polvo se ha asentado y se garantiza que todos los efectos secundarios que se han visto hasta ahora están completos.


¿Cuáles son los puntos de secuencia comunes enumerados en el estándar C ++?

Esos son:

  • al final de la evaluación de la expresión completa ( §1.9/16) (Una expresión completa es una expresión que no es una subexpresión de otra expresión). 1

    Ejemplo:

    int a = 5; // ; is a sequence point here
  • en la evaluación de cada una de las siguientes expresiones después de la evaluación de la primera expresión ( §1.9/18) 2

    • a && b (§5.14)
    • a || b (§5.15)
    • a ? b : c (§5.16)
    • a , b (§5.18)(aquí a, b es un operador de coma; en func(a,a++) ,no es un operador de coma, es simplemente un separador entre los argumentos ay a++. Por lo tanto, el comportamiento no está definido en ese caso (si ase considera un tipo primitivo))
  • en una llamada a la función (si la función está en línea o no), 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 ( §1.9/17).

1: Nota: la evaluación de una expresión completa puede incluir la evaluación de subexpresiones que no son léxicamente parte de la expresión completa. Por ejemplo, se considera que las subexpresiones involucradas en la evaluación de expresiones de argumento predeterminadas (8.3.6) se crean en la expresión que llama a la función, no en la expresión que define el argumento predeterminado

2: Los operadores indicados son los operadores integrados, como se describe en la cláusula 5. Cuando uno de estos operadores está sobrecargado (cláusula 13) en un contexto válido, designando así una función de operador definida por el usuario, la expresión designa una invocación de función y los operandos forman una lista de argumentos, sin un punto de secuencia implícito entre ellos.


¿Qué es el comportamiento indefinido?

El Estándar define Comportamiento Indefinido en la Sección §1.3.12como

comportamiento, tal como podría surgir con el uso de una construcción de programa errónea o datos erróneos, para lo cual esta Norma Internacional no impone requisitos 3 .

También se puede esperar un comportamiento indefinido cuando esta Norma Internacional omite la descripción de cualquier definición explícita de comportamiento.

3: el comportamiento indefinido permitido varía desde ignorar la situación por completo con resultados impredecibles, hasta comportarse durante la traducción o la ejecución del programa de una manera documentada característica del entorno (con o sin la emisión de un mensaje de diagnóstico), hasta terminar una traducción o ejecución (con la emisión de un mensaje de diagnóstico).

En resumen, un comportamiento indefinido significa que cualquier cosa puede suceder, desde demonios volando por la nariz hasta que tu novia quede embarazada.


¿Cuál es la relación entre el comportamiento indefinido y los puntos de secuencia?

Antes de entrar en eso, debe conocer la (s) diferencia (s) entre Comportamiento indefinido, Comportamiento no especificado e Comportamiento definido de implementación .

También debes saber eso the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified.

Por ejemplo:

int x = 5, y = 6;

int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first.

Otro ejemplo aquí .


Ahora el estándar en §5/4dice

  • 1) Entre el punto de secuencia anterior y el siguiente, un objeto escalar tendrá su valor almacenado modificado como máximo una vez mediante la evaluación de una expresión.

Qué significa eso?

Informalmente significa que entre dos puntos de secuencia una variable no debe modificarse más de una vez. En una declaración de expresión, next sequence pointusualmente está en el punto y coma final, y previous sequence pointestá al final de la declaración anterior. Una expresión también puede contener intermedios sequence points.

De la oración anterior, las siguientes expresiones invocan Comportamiento indefinido:

i++ * ++i;   // UB, i is modified more than once btw two SPs
i = ++i;     // UB, same as above
++i = 2;     // UB, same as above
i = ++i + 1; // UB, same as above
++++++i;     // UB, parsed as (++(++(++i)))

i = (i, ++i, ++i); // UB, there's no SP between `++i` (right most) and assignment to `i` (`i` is modified more than once btw two SPs)

Pero las siguientes expresiones están bien:

i = (i, ++i, 1) + 1; // well defined (AFAIK)
i = (++i, i++, i);   // well defined 
int j = i;
j = (++i, i++, j*i); // well defined

  • 2) Además, se debe acceder al valor anterior solo para determinar el valor a almacenar.

Qué significa eso? Significa que si un objeto se escribe dentro de una expresión completa, todos y cada uno de los accesos dentro de la misma expresión deben estar directamente involucrados en el cálculo del valor a escribir .

Por ejemplo, en i = i + 1todos los accesos de i(en LHS y en RHS) están directamente involucrados en el cálculo del valor a escribir. Entonces está bien.

Esta regla efectivamente restringe las expresiones legales a aquellas en las que los accesos preceden demostrablemente a la modificación.

Ejemplo 1:

std::printf("%d %d", i,++i); // invokes Undefined Behaviour because of Rule no 2

Ejemplo 2

a[i] = i++ // or a[++i] = i or a[i++] = ++i etc

no está permitido porque uno de los accesos de i(el que está en a[i]) no tiene nada que ver con el valor que termina siendo almacenado en i (que sucede de nuevo en i++), por lo que no hay una buena manera de definirlo, ya sea para nuestro entendimiento o para el compilador: si el acceso debe tener lugar antes o después de que se almacene el valor incrementado. Entonces el comportamiento es indefinido.

Ejemplo 3

int x = i + i++ ;// Similar to above

Siga la respuesta para C ++ 11 aquí .

Prasoon Saurav
fuente
45
*p++ = 4 no es un comportamiento indefinido. *p++se interpreta como *(p++). p++devuelve p(una copia) y el valor almacenado en la dirección anterior. ¿Por qué invocaría eso a UB? Está perfectamente bien.
Prasoon Saurav
77
@ Mike: AFAIK, no hay copias (legales) del Estándar C ++ a las que pueda vincular.
sbi
11
Bueno, entonces podría tener un enlace a la página de pedido relevante de ISO. De todos modos, al pensarlo, la frase "conocimiento elemental de C ++ Standard" parece un poco contradictoria en términos, ya que si estás leyendo el estándar, estás pasando el nivel elemental. ¿Quizás podríamos enumerar qué cosas en el lenguaje necesita una comprensión básica, como la sintaxis de expresión, el orden de las operaciones y tal vez la sobrecarga del operador?
Mike DeSimone
41
No estoy seguro de que citar el estándar sea la mejor manera de enseñar a los novatos
Inverso
66
@Adrian La primera expresión invoca una UB porque no hay un punto de secuencia entre la última ++iy la asignación a i. La segunda expresión no invoca a UB porque la expresión ino cambia el valor de i. En el segundo ejemplo, i++le sigue un punto de secuencia ( ,) antes de llamar al operador de asignación.
Kolyunya
276

Este es un seguimiento de mi respuesta anterior y contiene material relacionado con C ++ 11. .


Prerrequisitos : Un conocimiento elemental de Relaciones (Matemáticas).


¿Es cierto que no hay puntos de secuencia en C ++ 11?

¡Si! Esto es muy cierto.

Puntos de secuencia se han sustituido por Secuenciado Antes y secuenciado Después (y no secuenciados y indeterminadamente Secuenciado ) las relaciones en C ++ 11.


¿Qué es exactamente esta cosa 'secuenciada antes'?

Secuenciado antes (§1.9 / 13) es una relación que es:

entre evaluaciones ejecutadas por un solo hilo e induce un estricto orden parcial 1

Formalmente significa dar dos evaluaciones cualesquiera (ver más abajo) A y B, si Ase secuencia antes B , entonces la ejecución de A deberá preceder a la ejecución de B. Si Ano está secuenciado antes By Bno está secuenciado antes A, entonces Ay no Bestán secuenciados 2 .

Las evaluaciones Ay Bse secuencian de manera indeterminada cuando Ase secuencia antes Bo Bse secuencia antes A, pero no se especifica qué 3 .

[Notas]
1: Una orden parcial estricto es una relación binaria "<" sobre un conjunto Pque es asymmetric, y transitive, es decir, para todos a, by cen P, tenemos que:
........ (i). si a <b entonces ¬ (b <a) ( asymmetry);
........ (ii). si a <b y b <c entonces a <c ( transitivity).
2: La ejecución de evaluaciones no secuenciadas puede superponerse .
3: Las evaluaciones secuenciadas indeterminadamente no pueden superponerse , pero cualquiera de las dos podría ejecutarse primero.


¿Cuál es el significado de la palabra 'evaluación' en el contexto de C ++ 11?

En C ++ 11, la evaluación de una expresión (o una subexpresión) en general incluye:

Ahora (§1.9 / 14) dice:

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

  • Ejemplo trivial:

    int x; x = 10; ++x;

    El cálculo del valor y el efecto secundario asociado con ++xse secuencian después del cálculo del valor y el efecto secundario dex = 10;


Entonces, debe haber alguna relación entre el Comportamiento Indefinido y las cosas mencionadas anteriormente, ¿verdad?

¡Si! Derecha.

En (§1.9 / 15) se ha mencionado que

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

Por ejemplo :

int main()
{
     int num = 19 ;
     num = (num << 3) + (num >> 3);
} 
  1. La evaluación de los operandos del +operador no está secuenciada entre sí.
  2. La evaluación de operandos <<y >>operadores no está secuenciada entre sí.

4: En una expresión que se evalúa más de una vez durante la ejecución de un programa, unsequenced y indeterminadamente secuenciaron evaluaciones de sus subexpresiones no tienen que llevarse a cabo constantemente en diferentes evaluaciones.

(§1.9 / 15) Los cálculos del valor de los operandos de un operador se secuencian antes del cálculo del valor del resultado del operador.

Eso significa que en x + yel cálculo del valor de xy yse secuencian antes del cálculo del valor de (x + y).

Más importante

(§1.9 / 15) Si un efecto secundario en un objeto escalar no está secuenciado en relación con

(a) otro efecto secundario sobre el mismo objeto escalar

o

(b) un cálculo de valor utilizando el valor del mismo objeto escalar.

El comportamiento es indefinido .

Ejemplos:

int i = 5, v[10] = { };
void  f(int,  int);
  1. i = i++ * ++i; // Undefined Behaviour
  2. i = ++i + i++; // Undefined Behaviour
  3. i = ++i + ++i; // Undefined Behaviour
  4. i = v[i++]; // Undefined Behaviour
  5. i = v[++i]: // Well-defined Behavior
  6. i = i++ + 1; // Undefined Behaviour
  7. i = ++i + 1; // Well-defined Behaviour
  8. ++++i; // Well-defined Behaviour
  9. f(i = -1, i = -1); // Undefined Behaviour (see below)

Al llamar a una función (ya sea que la función esté en línea o no), cada cálculo de valor y efecto secundario asociado con cualquier expresión de argumento, o con la expresión de postfijo 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 [ Nota: los cálculos de valores y los efectos secundarios asociados con diferentes expresiones de argumento no están secuenciados . - nota final ]

Expresiones (5), (7)y (8)no invocan comportamientos indefinidos. Consulte las siguientes respuestas para obtener una explicación más detallada.


Nota final :

Si encuentra algún defecto en la publicación, deje un comentario. Usuarios avanzados (con rep> 20000) no duden en editar la publicación para corregir errores tipográficos y otros errores.

Prasoon Saurav
fuente
3
En lugar de "asimétrico", secuenciado antes / después son relaciones "antisimétricas". Esto debería cambiarse en el texto para ajustarse a la definición de un orden parcial que se da más adelante (que también está de acuerdo con Wikipedia).
TemplateRex
1
¿Por qué el artículo 7) en el último ejemplo es un UB? Tal vez debería ser f(i = -1, i = 1)?
Mikhail
1
Arreglé la descripción de la relación "secuenciado antes". Es un estricto orden parcial . Obviamente, una expresión no puede secuenciarse antes que ella misma, por lo que la relación no puede ser reflexiva. Por lo tanto, es asimétrico, no antisimétrico.
ThomasMcLeod
1
5) estar bien mejorado me dejó boquiabierto. La explicación de Johannes Schaub no fue del todo fácil de entender. Especialmente porque creía que incluso en ++i(siendo evaluado el valor antes del +operador que lo está usando), el estándar todavía no dice que su efecto secundario debe ser terminado. Pero, de hecho, debido a que devuelve una referencia a una lvalueque es en isí misma, DEBE haber terminado el efecto secundario ya que la evaluación debe estar terminada, por lo tanto, el valor debe estar actualizado. Esta fue la parte loca de hecho.
v.oddou
"Los miembros del Comité ISO C ++ pensaron que las cosas de los puntos de secuencia eran bastante difíciles de entender. Por lo tanto, decidieron reemplazarlo con las relaciones antes mencionadas solo para una redacción más clara y una mayor precisión". - ¿Tienes una referencia para ese reclamo? Me parece que las nuevas relaciones son más difíciles de entender.
MM
30

C ++ 17 ( N4659) incluye una propuesta de Orden de evaluación de expresión de refinación para Idiomatic C ++ que define un orden de evaluación de expresión más estricto.

En particular, la siguiente oración

8.18 Operadores de asignación y asignación compuesta :
....

En todos los casos, la asignación se secuencia después del cálculo del valor de los operandos derecho e izquierdo, y antes del cálculo del valor de la expresión de asignación. El operando derecho se secuencia antes que el operando izquierdo.

junto con la siguiente aclaración

Una expresión X se dice que está secuenciado antes de una expresión Y si cada valor de cálculo y cada efecto secundario asociado con la expresión X se secuencia antes de cada valor de cálculo y cada efecto secundario asociado con la expresión Y .

Hacer válidos varios casos de comportamiento previamente indefinido, incluido el en cuestión:

a[++i] = i;

Sin embargo, varios otros casos similares todavía conducen a un comportamiento indefinido.

En N4140:

i = i++ + 1; // the behavior is undefined

Pero en N4659

i = i++ + 1; // the value of i is incremented
i = i++ + i; // the behavior is undefined

Por supuesto, el uso de un compilador compatible con C ++ 17 no significa necesariamente que uno deba comenzar a escribir tales expresiones.

AlexD
fuente
por qué i = i++ + 1;se define el comportamiento en c ++ 17, creo que incluso si "El operando derecho se secuencia antes que el operando izquierdo", sin embargo, la modificación de "i ++" y el efecto secundario para la asignación no están secuenciados, proporcione más detalles para interpretarlos
Jack X
@jackX extendí la respuesta :).
AlexD
sí, creo que el detalle de la interpretación de la oración "El operando derecho se secuencia antes del operando izquierdo" es más útil. Por ejemplo, "El operando derecho se secuencia antes del operando izquierdo" significa que el cálculo del valor y el efecto secundario asociado con el operando derecho son secuenciado antes que el del operando izquierdo. como lo hiciste :-)
jack X
11

Supongo que hay una razón fundamental para el cambio, no es meramente cosmético aclarar la vieja interpretación: esa razón es la concurrencia. El orden de elaboración no especificado es meramente la selección de uno de varios posibles pedidos en serie, esto es bastante diferente a los pedidos anteriores y posteriores, porque si no hay un pedido específico, es posible una evaluación concurrente: no es así con las viejas reglas. Por ejemplo en:

f (a,b)

anteriormente, ya sea a b, o, b entonces a. Ahora, ayb pueden evaluarse con instrucciones intercaladas o incluso en diferentes núcleos.

Yttrill
fuente
55
Sin embargo, creo que si 'a' o 'b' incluyen una llamada a la función, se secuencian de forma indeterminada en lugar de no secuenciarse, lo que significa que todos los efectos secundarios de uno deben ocurrir antes de cualquier efecto secundario del otro, aunque el compilador no necesita ser consistente sobre cuál va primero. Si eso ya no es cierto, rompería una gran cantidad de código que depende de que las operaciones no se superpongan (por ejemplo, si 'a' y 'b' configuran, usan y eliminan, un estado estático compartido).
supercat
2

En lo C99(ISO/IEC 9899:TC3)que parece ausente de esta discusión hasta ahora, se hacen los siguientes steteents con respecto al orden de evaluación.

[...] el orden de evaluación de las subexpresiones y el orden en que se producen los efectos secundarios no están especificados. (Sección 6.5 pág. 67)

El orden de evaluación de los operandos no está especificado. Si se intenta modificar el resultado de un operador de asignación o acceder a él después del siguiente punto de secuencia, el comportamiento [sic] no está definido (Sección 6.5.16, pág. 91)

Awiebe
fuente
2
La pregunta está etiquetada como C ++ y no como C, lo cual es bueno porque el comportamiento en C ++ 17 es bastante diferente del comportamiento en versiones anteriores, y no tiene relación con el comportamiento en C11, C99, C90, etc. O tiene muy poco relación con ella. En general, sugeriría eliminar esto. Más significativamente, necesitamos encontrar el Q&A equivalente para C y asegurarnos de que esté bien (y observa que C ++ 17, en particular, cambia las reglas: el comportamiento en C ++ 11 y antes era más o menos el mismo que en C11, aunque la palabrería que lo describe en C todavía usa 'puntos de secuencia' mientras que C ++ 11 y más tarde no.
Jonathan Leffler