¿Qué son los puntos de secuencia y cómo se relacionan con el comportamiento indefinido?
¿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 sentirme 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 pretende ser una entrada a las preguntas frecuentes sobre C++ de Stack Overflow . Si desea criticar la idea de proporcionar preguntas frecuentes en este formulario, entonces la publicación en meta que inició todo esto sería el lugar para hacerlo. Respuestas a Esa pregunta se monitorea en la sala de chat de C++ , donde comenzó la idea de las preguntas frecuentes, por lo que es muy probable que su respuesta sea leída por aquellos a quienes se les ocurrió la idea).
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 están 'secuenciadas antes' o 'no secuenciadas' o 'secuenciadas indeterminadamente'. 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 de nuevo no te volverá loco.
Requisitos previos : conocimientos elementales del estándar C++.
¿Qué son los puntos de secuencia?
La norma 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án producido efectos secundarios de las evaluaciones posteriores. (§1.9/7)
¿Efectos secundarios? ¿Cuáles son los efectos secundarios?
La evaluación de una expresión produce algo y si además hay un cambio en el estado del entorno de ejecución se dice que la expresión (su evaluación) tiene algún efecto secundario.
Por ejemplo:
int x = y++; //where y is also an int
Además de la operación de inicialización, el valor de y
cambia debido al efecto secundario del ++
operador.
Hasta ahora, todo bien. Pasando a los puntos de secuencia. Una definición de alternancia de puntos secuenciales dada por el autor de comp.lang.c Steve Summit
:
El punto de secuencia es un momento en el que todo se ha asentado y se garantiza que todos los efectos secundarios observados hasta ahora será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). 1Ejemplo :
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
) 2a && b (§5.14)
a || b (§5.15)
a ? b : c (§5.16)
a , b (§5.18)
(aquí a , b es un operador de coma; infunc(a,a++)
,
no es un operador de coma, es simplemente un separador entre los argumentosa
ya++
. Por lo tanto, el comportamiento no está definido en ese caso (sia
se considera un tipo primitivo))
en una llamada de función (ya sea que 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, las subexpresiones involucradas en la evaluación de expresiones de argumentos predeterminados (8.3.6) se consideran creadas 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 el Comportamiento Indefinido en la Sección §1.3.12
como
comportamiento, como el que podría surgir tras el uso de una construcción de programa errónea o datos erróneos, para los cuales 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 completamente la situación con resultados impredecibles, hasta comportarse durante la traducción o 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 finalizar una traducción o ejecución. (con la emisión de un mensaje de diagnóstico).
¿Cuál es la relación entre el comportamiento indefinido y los puntos de secuencia?
Antes de entrar en eso, debe conocer la diferencia entre comportamiento indefinido, comportamiento no especificado y comportamiento definido por 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 §5/4
dice
-
- Entre el punto de secuencia anterior y el siguiente, el valor almacenado de un objeto escalar se modificará como máximo una vez mediante la evaluación de una expresión.
¿Qué significa?
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 point
generalmente está al final del punto y coma y previous sequence point
al final de la declaración anterior. Una expresión también puede contener intermedio sequence points
.
De la oración anterior, las siguientes expresiones invocan un 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
-
- Además, se accederá al valor anterior solo para determinar el valor que se almacenará.
¿Qué significa? Significa que si se escribe un objeto dentro de una expresión completa, todos y cada uno de los accesos a él dentro de la misma expresión deben estar directamente involucrados en el cálculo del valor que se escribirá .
Por ejemplo, en i = i + 1
todos los accesos i
(en LHS y en RHS) están directamente involucrados en el cálculo del valor a escribir. Entonces está bien.
Esta regla efectivamente constriñe las expresiones jurídicas 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 almacenándose en i (lo que sucede en i++
), por lo que no hay una buena manera de definirlo, ya sea para nuestro entendimiento o para el del compilador: si el acceso debe tener lugar antes o después de que se almacene el valor incrementado. Entonces el comportamiento no está definido.
Ejemplo 3:
int x = i + i++ ;// Similar to above
Respuesta de seguimiento para C++ 11 aquí .
Esta es una continuación de mi respuesta anterior y contiene material relacionado con C++11. .
Prerrequisitos : Conocimiento elemental de Relaciones (Matemáticas).
¿Es cierto que no hay puntos de secuencia en C++ 11?
¡Sí! Esto es muy cierto.
Los puntos de secuencia han sido reemplazados por relaciones Secuenciado antes y Secuenciado después (y Sin secuenciar y Secuenciado indeterminadamente ) en C++11.
¿Qué es exactamente eso de 'Secuenciado antes'?
Secuenciado antes (§1.9/13) es una relación que es:
- Asimétrico
- Transitivo
entre evaluaciones ejecutadas por un solo hilo e induce un orden parcial estricto 1
Formalmente significa dadas dos evaluaciones cualesquiera (ver más abajo) A
y B
, si A
se secuencia antes B
, entonces la ejecución de A
precederá a la ejecución de B
. Si A
no está secuenciado antes B
y B
no está secuenciado antes A
, entonces A
y no B
están secuenciados 2 .
Las evaluaciones A
y B
están secuenciadas de manera indeterminada cuando A
se secuencian antes B
o B
se secuencian antes A
, pero no se especifica cuál 3 .
[NOTAS]
1: Un orden parcial estricto es una relación binaria "<"
sobre un conjunto P
que es asymmetric
, y transitive
, es decir, para todo a
, b
, y c
en 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 con secuencia indeterminada no pueden superponerse , pero cualquiera de ellas 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:
cálculos de valor (incluida la determinación de la identidad de un objeto para la evaluación glvalue y la obtención de un valor previamente asignado a un objeto para la evaluación prvalue ) y
inicio de efectos secundarios .
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 que se evaluará .
Ejemplo trivial:
int x;
x = 10;
++x;
El cálculo del valor y los efectos secundarios asociados
++x
se secuencian después del cálculo del valor y los efectos secundarios dex = 10;
Entonces debe haber alguna relación entre el comportamiento indefinido y las cosas mencionadas anteriormente, ¿verdad?
¡Sí! Bien.
En (§1.9/15) se ha mencionado que
Excepto donde se indique lo contrario, las evaluaciones de operandos de operadores individuales y de subexpresiones de expresiones individuales no están secuenciadas 4 .
Por ejemplo :
int main()
{
int num = 19 ;
num = (num << 3) + (num >> 3);
}
- La evaluación de los operandos del
+
operador no está secuenciada entre sí. - 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, no es necesario realizar evaluaciones no secuenciadas y con secuencia indeterminada de sus subexpresiones de manera consistente 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 + y
el cálculo del valor de x
y y
se 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 cualquiera de los dos
(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 no está definido .
Ejemplos:
int i = 5, v[10] = { };
void f(int, int);
i = i++ * ++i; // Undefined Behaviour
i = ++i + i++; // Undefined Behaviour
i = ++i + ++i; // Undefined Behaviour
i = v[i++]; // Undefined Behaviour
i = v[++i]: // Well-defined Behavior
i = i++ + 1; // Undefined Behaviour
i = ++i + 1; // Well-defined Behaviour
++++i; // Well-defined Behaviour
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 sufijo que designa la función llamada, se secuencia antes de la ejecución de cada expresión o declaración en el cuerpo de la función. llamada función. [ Nota: los cálculos de valores y los efectos secundarios asociados con diferentes expresiones de argumentos no están secuenciados . - nota final ]
Expresiones (5)
y (7)
no (8)
invocan comportamientos indefinidos. Consulte las siguientes respuestas para obtener una explicación más detallada.
- Múltiples operaciones de preincremento en una variable en C++ 0x
- Cálculos de valores no secuenciados
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.
C++ 17 ( N4659
) incluye una propuesta para Refinar el orden de evaluación de expresiones para Idiomatic C++
que define un orden más estricto de evaluación de expresiones.
En particular, la siguiente frase
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
Se dice que una expresión X está secuenciada antes de una expresión Y si cada cálculo de valor y cada efecto secundario asociado con la expresión X se secuencia antes de cada cálculo de valor y cada efecto secundario asociado con la expresión Y.
hacer válidos varios casos de comportamiento previamente indefinido, incluido el que nos ocupa:
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 enN4659
i = i++ + 1; // the value of i is incremented
i = i++ + i; // the behavior is undefined
Por supuesto, usar un compilador compatible con C++ 17 no significa necesariamente que uno deba comenzar a escribir dichas expresiones.
Supongo que hay una razón fundamental para el cambio; no es meramente cosmético aclarar la antigua interpretación: esa razón es la concurrencia. El orden de elaboración no especificado es simplemente la selección de uno de varios ordenamientos en serie posibles, esto es bastante diferente a los ordenamientos antes y después, porque si no hay un orden específico, la evaluación concurrente es posible: no ocurre así con las reglas antiguas. Por ejemplo en:
f (a,b)
anteriormente a, luego b, o b, luego a. Ahora, a y b se pueden evaluar con instrucciones entrelazadas o incluso en diferentes núcleos.