Comportamiento indefinido y puntos de secuencia

¿Qué son “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 debe 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 inició todo esto sería el lugar para hacerlo). esa pregunta se monitorea en la sala de chat de C ++ , donde la idea de las 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).

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 indeterminadas’ en su lugar. El efecto neto es esencialmente el mismo, pero la terminología es diferente.


Descargo de responsabilidad : 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 : conocimiento elemental del estándar C ++


¿Qué son los puntos de secuencia?

El estándar dice

En ciertos puntos específicos de la secuencia de ejecución denominados puntos de secuencia , todos los efectos secundarios de las evaluaciones previas deberán estar completos y no se habrán producido los 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 se modifica debido al efecto secundario del operador ++ .

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

El punto de secuencia es un punto en el tiempo en el cual el polvo se asentó y todos los efectos secundarios que se han visto hasta el momento están garantizados para completarse.


¿Cuáles son los puntos de secuencia comunes enumerados en el estándar de 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 de función (si la función está en línea o no), después de la evaluación de todos los argumentos de función (si los hay) que tienen lugar antes de la ejecución de cualquier expresión o enunciado en el cuerpo de 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 por defecto (8.3.6) se consideran creadas en la expresión que llama a la función, no la expresión que define el argumento predeterminado

2: Los operadores indicados son los operadores incorporados, 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 un comportamiento indefinido?

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

comportamiento, tal como podría surgir al usar una construcción de progtwig 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 permisible varía desde ignorar completamente la situación con resultados impredecibles, hasta comportarse durante la traducción o la ejecución del progtwig 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 que salen volando de tu nariz hasta que tu novia queda embarazada.


¿Cuál es la relación entre el Comportamiento Indefinido y los Puntos de Secuencia?

Antes de entrar en eso, debe conocer la diferencia (s) entre el Comportamiento Indefinido, el Comportamiento No Especificado y el Comportamiento Definido por la Implementación .

También debe saber que 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 siguiente, un objeto escalar tendrá su valor almacenado modificado como máximo una vez por 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 statement de expresión, el next sequence point generalmente está en el punto y coma de terminación, y el previous sequence point está al final de la statement anterior. Una expresión también puede contener sequence points intermedios.

A partir de la oración anterior, las siguientes expresiones invocan Comportamiento no definido:

 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 accederá al valor anterior solo para determinar el valor que se 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 que se escribirá .

Por ejemplo, en i = i + 1 todo el acceso de i (en LHS y en RHS) está directamente involucrado en el cálculo del valor a escribir. Entonces está bien.

Esta regla efectivamente limita las expresiones legales a aquellas en las que los accesos preestablecen 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 se permite porque uno de los accesos de i (el que está en a[i] ) no tiene nada que ver con el valor que termina almacenado en i (que pasa en i++ ), por lo que no hay una buena manera de definir– ya sea para nuestro entendimiento o para el del comstackdor: 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 aquí .

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?

¡Sí! Esto es muy cierto.

Los puntos de secuencia han sido reemplazados por relaciones secuenciadas después (y secuencia no secuenciada e indeterminada ) en C ++ 11.


¿Qué es exactamente esto ‘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 una orden parcial estricta 1

Formalmente significa dadas dos evaluaciones cualquiera (Ver abajo) A y B , si A se secuencia antes de B , entonces la ejecución de A debe preceder a la ejecución de B Si A no está secuenciado antes de que B y B no se secuencian antes de A , entonces A y B son secuenciados 2 .

Las evaluaciones A y B se secuencian indefinidamente cuando A se secuencia antes de que B o B se secuencian antes que A , pero no se especifica qué 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:
........(yo). si a asymmetry );
…….. (ii). si a transitivity ).
2: la ejecución de evaluaciones no secuenciadas puede superponerse .
3: las evaluaciones secuenciadas indefinidamente no se pueden superponer , pero cualquiera de ellas se podría ejecutar 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 sub-expresión) en general incluye:

  • cómputos de valor (incluida la determinación de la identidad de un objeto para la evaluación de 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ómputo de valor y efecto secundario asociado con una expresión completa se secuencia antes de que se evalúe cada cálculo de valor y efecto secundario asociado con la siguiente expresión completa .

  • Ejemplo trivial:

    int x; x = 10; ++x;

    El cómputo del valor y los efectos secundarios asociados con ++x se secuencian después del cálculo del valor y el efecto secundario de x = 10;


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

¡Sí! 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 son secuenciadas 4 .

Por ejemplo :

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

4: en una expresión que se evalúa más de una vez durante la ejecución de un progtwig, las evaluaciones no secuenciadas e indeterminadas de sus subexpresiones no necesitan realizarse de manera consistente en diferentes evaluaciones.

(§1.9 / 15) Los cómputos 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ómputo del valor de y se secuencia antes del cálculo del valor de (x + y) .

Más importante

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

(a) otro efecto secundario en el mismo objeto escalar

o

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

el comportamiento no está definido

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)

Cuando se llama a una función (esté o no en línea), cada cómputo 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 statement en el cuerpo del llamada función. [ Nota: los cómputos de valor y los efectos secundarios asociados con las diferentes expresiones de argumentos no se han secuenciado . - nota final ]

Las expresiones (5) , (7) y (8) no invocan un comportamiento indefinido. Vea las siguientes respuestas para una explicación más detallada.

  • Múltiples operaciones de preincremento en una variable en C ++ 0x
  • Cálculos de valor no secuenciados

Nota final :

Si encuentra algún error en la publicación, por favor deje un comentario. Los usuarios avanzados (con representante> 20000) no duden en editar la publicación para corregir errores tipográficos y otros errores.

C ++ 17 ( N4659 ) incluye una propuesta Refining Expression Evaluation Order para Idiomatic C ++ que define un orden más estricto de evaluación de expresiones.

En particular, se agregó la siguiente oración :

8.18 Asignación y operadores de 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 del operando izquierdo.

Hace que varios casos de comportamiento previamente indefinido sean válidos, incluido el que está 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 comstackdor compatible con C ++ 17 no significa necesariamente que uno deba comenzar a escribir tales 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 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 especificado, la evaluación concurrente es posible: no ocurre lo mismo con las reglas anteriores. Por ejemplo en:

 f (a,b) 

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