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 y
se 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 a
y a++
. Por lo tanto, el comportamiento no está definido en ese caso (si a
se 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.12
como
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/4
dice
- 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 point
usualmente está en el punto y coma final, y previous sequence point
está 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 + 1
todos 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í .
*p++ = 4
no es un comportamiento indefinido.*p++
se interpreta como*(p++)
.p++
devuelvep
(una copia) y el valor almacenado en la dirección anterior. ¿Por qué invocaría eso a UB? Está perfectamente bien.