Estaba leyendo sobre las infracciones del orden de evaluación , y dan un ejemplo que me desconcierta.
1) Si un efecto secundario en un objeto escalar no está secuenciado en relación con otro efecto secundario en el mismo objeto escalar, el comportamiento es indefinido.
// snip f(i = -1, i = -1); // undefined behavior
En este contexto, i
es un objeto escalar , lo que aparentemente significa
Los tipos aritméticos (3.9.1), los tipos de enumeración, los tipos de puntero, los tipos de puntero a miembro (3.9.2), std :: nullptr_t y las versiones calificadas por cv de estos tipos (3.9.3) se denominan colectivamente tipos escalares.
No veo cómo la declaración es ambigua en ese caso. Me parece que independientemente de si el primer o segundo argumento se evalúa primero, i
termina como -1
, y ambos argumentos también lo son -1
.
¿Alguien puede aclarar?
ACTUALIZAR
Realmente aprecio toda la discusión. Hasta ahora, me gusta mucho la respuesta de @ harmic, ya que expone las trampas y las complejidades de definir esta declaración a pesar de lo directa que parece a primera vista. @ acheong87 señala algunos problemas que surgen al usar referencias, pero creo que eso es ortogonal al aspecto de efectos secundarios no secuenciados de esta pregunta.
RESUMEN
Como esta pregunta recibió mucha atención, resumiré los puntos / respuestas principales. Primero, permítanme una pequeña digresión para señalar que "por qué" puede tener significados estrechamente relacionados pero sutilmente diferentes, a saber, "por qué causa ", "por qué razón " y "con qué propósito ". Agruparé las respuestas según cuál de esos significados de "por qué" abordaron.
por que causa
La respuesta principal aquí proviene de Paul Draper , con Martin J contribuyendo con una respuesta similar pero no tan extensa. La respuesta de Paul Draper se reduce a
Es un comportamiento indefinido porque no está definido cuál es el comportamiento.
La respuesta es en general muy buena en términos de explicar lo que dice el estándar C ++. También aborda algunos casos relacionados de UB como f(++i, ++i);
y f(i=1, i=-1);
. En el primero de los casos relacionados, no está claro si el primer argumento debería ser i+1
y el segundo i+2
o viceversa; en el segundo, no está claro si i
debería ser 1 o -1 después de la llamada a la función. Ambos casos son UB porque caen bajo la siguiente regla:
Si un efecto secundario en un objeto escalar no está secuenciado en relación con otro efecto secundario en el mismo objeto escalar, el comportamiento es indefinido.
Por lo tanto, f(i=-1, i=-1)
también es UB ya que cae bajo la misma regla, a pesar de que la intención del programador es (en mi humilde opinión) obvia e inequívoca.
Paul Draper también lo hace explícito en su conclusión de que
¿Podría haber sido un comportamiento definido? Si. ¿Fue definido? No.
lo que nos lleva a la pregunta de "¿por qué razón / propósito se f(i=-1, i=-1)
dejó como comportamiento indefinido?"
por qué razón / propósito
Aunque hay algunos descuidos (quizás descuidados) en el estándar C ++, muchas omisiones están bien razonadas y tienen un propósito específico. Aunque soy consciente de que el propósito es a menudo "facilitar el trabajo del compilador-escritor" o "código más rápido", estaba interesado principalmente en saber si hay una buena razón para dejar f(i=-1, i=-1)
UB.
harmic y supercat proporcionan las principales respuestas que proporcionan una razón para la UB. Harmic señala que un compilador optimizador que podría dividir las operaciones de asignación aparentemente atómica en múltiples instrucciones de máquina, y que podría entrelazar aún más esas instrucciones para una velocidad óptima. Esto podría conducir a algunos resultados muy sorprendentes: ¡ i
termina en -2 en su escenario! Por lo tanto, harmic demuestra cómo asignar el mismo valor a una variable más de una vez puede tener efectos negativos si las operaciones no están secuenciadas.
Supercat proporciona una exposición relacionada de las trampas de tratar f(i=-1, i=-1)
de hacer lo que parece que debería hacer. Señala que en algunas arquitecturas, existen restricciones estrictas contra múltiples escrituras simultáneas en la misma dirección de memoria. Un compilador podría tener dificultades para detectar esto si estuviéramos tratando con algo menos trivial que f(i=-1, i=-1)
.
davidf también proporciona un ejemplo de instrucciones de entrelazado muy similares a las de harmic.
Aunque cada uno de los ejemplos de harmic's, supercat's y davidf 'son algo inventados, en conjunto todavía sirven para proporcionar una razón tangible de por qué f(i=-1, i=-1)
debería ser un comportamiento indefinido.
Acepté la respuesta de Harmic porque hizo el mejor trabajo al abordar todos los significados de por qué, a pesar de que la respuesta de Paul Draper abordó mejor la parte "por qué causa".
Otras respuestas
JohnB señala que si consideramos operadores de asignación sobrecargados (en lugar de simples escalares), entonces también podemos tener problemas.
fuente
std::nullptr_t
y las versiones calificadas por cv de estos tipos (3.9.3) se denominan colectivamente tipos escalares . "f(i-1, i = -1)
o algo similar.Respuestas:
Como las operaciones no están secuenciadas, no hay nada que decir que las instrucciones que realizan la asignación no se pueden intercalar. Puede ser óptimo hacerlo, dependiendo de la arquitectura de la CPU. La página referenciada dice esto:
Eso en sí mismo no parece causar un problema, suponiendo que la operación que se realiza es almacenar el valor -1 en una ubicación de memoria. Pero tampoco hay nada que decir que el compilador no puede optimizar eso en un conjunto separado de instrucciones que tiene el mismo efecto, pero que podría fallar si la operación se intercala con otra operación en la misma ubicación de memoria.
Por ejemplo, imagine que era más eficiente poner a cero la memoria, luego disminuirla, en comparación con cargar el valor -1 pulg. Entonces esto:
podría convertirse:
Ahora tengo -2.
Probablemente sea un ejemplo falso, pero es posible.
fuente
load 8bit immediate and shift
hasta 4 veces. Por lo general, el compilador realizará un direccionamiento indirecto para obtener un número de una tabla para evitar esto. (-1 puede hacerse en 1 instrucción, pero podría elegirse otro ejemplo).Primero, "objeto escalar" significa un tipo como a
int
,float
o un puntero (ver ¿Qué es un objeto escalar en C ++? ).En segundo lugar, puede parecer más obvio que
tendría un comportamiento indefinido. Pero
Es menos obvio.
Un ejemplo ligeramente diferente:
¿Qué tarea sucedió "último"
i = 1
, oi = -1
? No está definido en el estándar. Realmente, ese medioi
podría ser5
(ver la respuesta de harmic para una explicación completamente plausible de cómo podría ser este el caso). O su programa podría segfault. O reformatee su disco duro.Pero ahora pregunta: "¿Qué pasa con mi ejemplo? Usé el mismo valor (
-1
) para ambas asignaciones. ¿Qué podría no estar claro al respecto?"Tienes razón ... excepto en la forma en que el comité de estándares de C ++ describió esto.
Ellos podrían haber hecho una excepción especial para su caso especial, pero no lo hicieron. (¿Y por qué deberían hacerlo? ¿Qué utilidad tendría alguna vez?) Entonces,
i
aún podría ser5
. O tu disco duro podría estar vacío. Por lo tanto, la respuesta a su pregunta es:Es un comportamiento indefinido porque no está definido cuál es el comportamiento.
(Esto merece énfasis porque muchos programadores piensan que "indefinido" significa "aleatorio" o "impredecible". No lo hace; significa que no está definido por el estándar. El comportamiento podría ser 100% consistente y aún no estar definido).
¿Podría haber sido un comportamiento definido? Si. ¿Fue definido? No. Por lo tanto, es "indefinido".
Dicho esto, "indefinido" no significa que un compilador formateará su disco duro ... significa que podría y seguiría siendo un compilador compatible con los estándares. Siendo realistas, estoy seguro de que g ++, Clang y MSVC harán lo que esperabas. Simplemente no "tendrían que".
Una pregunta diferente podría ser ¿Por qué el comité de estándares de C ++ eligió hacer que este efecto secundario no fuera secuenciado? . Esa respuesta involucrará la historia y las opiniones del comité. ¿ O qué tiene de bueno que este efecto secundario no esté secuenciado en C ++? , lo que permite cualquier justificación, sea o no el razonamiento real del comité de normas. Puede hacer esas preguntas aquí o en programmers.stackexchange.com.
fuente
-Wsequence-point
para g ++, te lo advertirá.undefined behavior
significasomething random will happen
, lo que está lejos de ser el caso la mayor parte del tiempo.Una razón práctica para no hacer una excepción a las reglas solo porque los dos valores son iguales:
Considere el caso, esto fue permitido.
Ahora, algunos meses después, surge la necesidad de cambiar
Aparentemente inofensivo, ¿no es así? Y, sin embargo, de repente prog.cpp ya no se compila. Sin embargo, creemos que la compilación no debe depender del valor de un literal.
En pocas palabras: no hay excepción a la regla porque haría que la compilación exitosa dependiera del valor (más bien del tipo) de una constante.
EDITAR
@HeartWare señaló que las expresiones constantes del formulario
A DIV B
no están permitidas en algunos idiomas, cuandoB
es 0, y hacen que la compilación falle. Por lo tanto, el cambio de una constante podría causar errores de compilación en algún otro lugar. Lo cual es, en mi humilde opinión, lamentable. Pero ciertamente es bueno restringir tales cosas a lo inevitable.fuente
f(i = VALUEA, i = VALUEB);
tiene definitivamente el potencial para un comportamiento indefinido. Espero que no estés realmente codificando contra valores detrás de los identificadores.SomeProcedure(A, B, B DIV (2-A))
. De todos modos, si el lenguaje establece que CONST debe evaluarse completamente en el momento de la compilación, entonces, por supuesto, mi reclamo no es válido para ese caso. Dado que de alguna manera borra la distinción de tiempo de compilación y tiempo de ejecución. ¿También se daría cuenta si escribimosCONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y;
? ¿O no están permitidas las funciones?La confusión es que almacenar un valor constante en una variable local no es una instrucción atómica en cada arquitectura en la que el C está diseñado para ejecutarse. El procesador en el que se ejecuta el código importa más que el compilador en este caso. Por ejemplo, en ARM donde cada instrucción no puede llevar una constante completa de 32 bits, almacenar un int en una variable necesita más de una instrucción. Ejemplo con este pseudocódigo donde solo puede almacenar 8 bits a la vez y debe funcionar en un registro de 32 bits, i es un int32:
Puede imaginar que si el compilador quiere optimizarlo puede intercalar la misma secuencia dos veces, y no sabe qué valor se escribirá en i; y digamos que no es muy inteligente:
Sin embargo, en mis pruebas, gcc es lo suficientemente amable como para reconocer que el mismo valor se usa dos veces y lo genera una vez y no hace nada extraño. Obtengo -1, -1 Pero mi ejemplo sigue siendo válido, ya que es importante tener en cuenta que incluso una constante puede no ser tan obvia como parece.
fuente
-1
(que el compilador ha almacenado en alguna parte), sino que es3^81 mod 2^32
constante pero constante, entonces el compilador puede hacer exactamente lo que se hace aquí, y en alguna palanca de optimización, intercalo las secuencias de llamadas para evitar esperarf(i = A, j = B)
dondei
yj
son dos objetos separados. Este ejemplo no tiene UB. La máquina que tiene 3 registros cortos no es excusa para que el compilador mezcle los dos valores deA
yB
en el mismo registro (como se muestra en la respuesta de @ davidf), porque rompería la semántica del programa.Comportamiento se especifica comúnmente como indefinido si hay alguna razón concebible por la cual un compilador que intentaba ser "útil" podría hacer algo que causaría un comportamiento totalmente inesperado.
En el caso de que una variable se escriba varias veces sin nada para garantizar que las escrituras sucedan en momentos distintos, algunos tipos de hardware pueden permitir que se realicen múltiples operaciones de "almacenamiento" simultáneamente en diferentes direcciones utilizando una memoria de doble puerto. Sin embargo, algunas memorias de doble puerto prohíben expresamente el escenario en el que dos tiendas alcanzan la misma dirección simultáneamente, independientemente de si los valores escritos coinciden o no.. Si un compilador para una máquina de este tipo nota dos intentos no secuenciados de escribir la misma variable, puede negarse a compilar o asegurarse de que las dos escrituras no puedan programarse simultáneamente. Pero si uno o ambos accesos se realizan a través de un puntero o una referencia, el compilador no siempre puede saber si ambas escrituras pueden llegar a la misma ubicación de almacenamiento. En ese caso, podría programar las escrituras simultáneamente, causando una trampa de hardware en el intento de acceso.
Por supuesto, el hecho de que alguien pueda implementar un compilador de C en una plataforma de este tipo no sugiere que dicho comportamiento no deba definirse en plataformas de hardware cuando se usan almacenes de tipos lo suficientemente pequeños como para ser procesados atómicamente. Intentar almacenar dos valores diferentes sin secuenciar podría causar rarezas si un compilador no lo sabe; por ejemplo, dado:
si el compilador incluye la llamada a "moo" y puede decir que no modifica "v", podría almacenar un 5 a v, luego almacenar un 6 a * p, luego pasar 5 al "zoológico" y luego pasar los contenidos de v a "zoo". Si "zoo" no modifica "v", no debería haber forma de que las dos llamadas pasen valores diferentes, pero eso podría suceder fácilmente de todos modos. Por otro lado, en los casos en que ambas tiendas escribirían el mismo valor, tal rareza no podría ocurrir y en la mayoría de las plataformas no habría una razón razonable para que una implementación haga algo extraño. Desafortunadamente, algunos escritores de compiladores no necesitan ninguna excusa para comportamientos tontos más allá de "porque el Estándar lo permite", por lo que incluso esos casos no son seguros.
fuente
El hecho de que el resultado sea el mismo en la mayoría de las implementaciones en este caso es incidental; el orden de evaluación aún no está definido. Considere
f(i = -1, i = -2)
: aquí, el orden importa. La única razón por la que no importa en su ejemplo es el accidente que tienen ambos valores-1
.Dado que la expresión se especifica como una con un comportamiento indefinido, un compilador malintencionado puede mostrar una imagen inapropiada cuando evalúa
f(i = -1, i = -1)
y cancela la ejecución, y aún así se considera completamente correcto. Afortunadamente, no hay compiladores que yo sepa que lo hagan.fuente
Me parece que la única regla relativa a la secuencia de la expresión de argumento de función está aquí:
Esto no define la secuencia entre expresiones de argumento, por lo que terminamos en este caso:
En la práctica, en la mayoría de los compiladores, el ejemplo que citó funcionará bien (en lugar de "borrar su disco duro" y otras consecuencias teóricas de comportamiento indefinido).
Sin embargo, es una responsabilidad, ya que depende del comportamiento específico del compilador, incluso si los dos valores asignados son iguales. Además, obviamente, si intentara asignar valores diferentes, los resultados serían "verdaderamente" indefinidos:
fuente
C ++ 17 define reglas de evaluación más estrictas. En particular, secuencia los argumentos de la función (aunque en un orden no especificado).
Permite algunos casos que serían UB antes:
fuente
f
la firma fueraf(int a, int b)
, ¿C ++ 17 garantiza esoa == -1
yb == -2
si se llama como en el segundo caso?a
yb
, entonces,i
-entonces-a
se inicializan a -1, luegoi
-entonces-b
se inicializan a -2, o al revés. En ambos casos, terminamos cona == -1
yb == -2
. Al menos así es como leo " La inicialización de un parámetro, incluyendo cada cálculo de valor asociado y efecto secundario, se secuencia indefinidamente con respecto a cualquier otro parámetro ".El operador de asignación podría estar sobrecargado, en cuyo caso el orden podría ser importante:
fuente
Esto solo responde a "No estoy seguro de lo que podría significar" objeto escalar "además de algo como un int o un flotante".
Interpretaría el "objeto escalar" como una abreviatura de "objeto de tipo escalar", o simplemente "variable de tipo escalar". Entonces,
pointer
,enum
(constante) son de tipo escalar.Este es un artículo de MSDN de Tipos escalares .
fuente
En realidad, hay una razón para no depender del hecho de que el compilador verificará que
i
esté asignado con el mismo valor dos veces, para que sea posible reemplazarlo con una sola asignación. ¿Qué pasa si tenemos algunas expresiones?fuente
1
ai
. O bien ambos argumentos asignan1
y esto hace lo "correcto", o los argumentos asignan valores diferentes y es un comportamiento indefinido, por lo que nuestra elección aún está permitida.