No está definido porque se modifica x
dos veces entre puntos de secuencia. El estándar dice que no está definido, por lo tanto, no está definido.
Eso lo sé.
¿Pero por qué?
Tengo entendido que prohibir esto permite a los compiladores optimizar mejor. Esto podría haber tenido sentido cuando se inventó C, pero ahora parece un argumento débil.
Si reinventamos C hoy, ¿lo haríamos de esta manera, o podríamos hacerlo mejor?
¿O tal vez hay un problema más profundo que hace que sea difícil definir reglas consistentes para tales expresiones, por lo que es mejor prohibirlas?
Supongamos que reinventamos C hoy. Me gustaría sugerir reglas simples para expresiones como x=x++
, que me parecen funcionar mejor que las reglas existentes.
Me gustaría conocer su opinión sobre las reglas sugeridas en comparación con las existentes u otras sugerencias.
Reglas sugeridas:
- Entre los puntos de secuencia, el orden de evaluación no está especificado.
- Los efectos secundarios tienen lugar de inmediato.
No hay un comportamiento indefinido involucrado. Las expresiones evalúan este valor o ese, pero seguramente no formatearán su disco duro (curiosamente, nunca he visto una implementación donde x=x++
formatee el disco duro).
Expresiones de ejemplo
x=x++
- Bien definido, no cambiax
.
Primero,x
se incrementa (inmediatamente cuandox++
se evalúa), luego se almacena su valor anteriorx
.x++ + ++x
- Incrementax
dos veces, evalúa a2*x+2
.
Aunque cualquier lado puede evaluarse primero, el resultado esx + (x+2)
(lado izquierdo primero) o(x+1) + (x+1)
(lado derecho primero).x = x + (x=3)
- Sin especificar,x
establecido enx+3
o6
.
Si el lado derecho se evalúa primero, esx+3
. También es posible quex=3
se evalúe primero, así es3+3
. En cualquier caso, lax=3
asignación ocurre inmediatamente cuandox=3
se evalúa, por lo que el valor almacenado se sobrescribe con la otra asignación.x+=(x=3)
- Bien definido, se establecex
en 6.
Se podría argumentar que esto es solo una forma abreviada de la expresión anterior.
Pero diría que+=
debe ejecutarse despuésx=3
, y no en dos partes (leerx
, evaluarx=3
, agregar y almacenar nuevo valor).
¿Cuál es la ventaja?
Algunos comentarios plantearon este buen punto.
Ciertamente no creo que expresiones como x=x++
deberían usarse en ningún código normal.
En realidad, soy mucho más estricto que eso: creo que el único buen uso x++
es x++;
solo.
Sin embargo, creo que las reglas del lenguaje deben ser lo más simples posible. De lo contrario, los programadores simplemente no los entienden. La regla que prohíbe cambiar una variable dos veces entre puntos de secuencia es ciertamente una regla que la mayoría de los programadores no entienden.
Una regla muy básica es esta:
si A es válido y B es válido, y se combinan de manera válida, el resultado es válido.
x
es un valor L válido, x++
es una expresión válida y =
es una forma válida de combinar un valor L y una expresión, entonces, ¿por qué x=x++
no es legal?
El estándar C hace una excepción aquí, y esta excepción complica las reglas. Puede buscar en stackoverflow.com y ver cuánto confunde esta excepción con las personas.
Así que digo: deshazte de esta confusión.
=== Resumen de respuestas ===
¿Por qué hacer eso?
Traté de explicarlo en la sección anterior: quiero que las reglas de C sean simples.Potencial de optimización:
esto toma algo de libertad del compilador, pero no vi nada que me convenciera de que podría ser significativo.
La mayoría de las optimizaciones aún se pueden hacer. Por ejemplo,a=3;b=5;
puede reordenarse, aunque el estándar especifique el orden. Expresiones comoa=b[i++]
todavía se pueden optimizar de manera similar.No puede cambiar el estándar existente.
Lo admito, no puedo. Nunca pensé que podría seguir adelante y cambiar los estándares y los compiladores. Solo quería pensar si las cosas podrían haberse hecho de manera diferente.
fuente
x
a sí mismo, y si desea aumentarx
, puede decirx++;
: no es necesario realizar la tarea. Yo diría que no debería definirse solo porque sería difícil recordar lo que se supone que sucederá.Respuestas:
¿Quizás debería primero responder la pregunta por qué debería definirse? ¿Hay alguna ventaja en el estilo de programación, legibilidad, mantenibilidad o rendimiento al permitir tales expresiones con efectos secundarios adicionales? Es
más legible que
Dado que dicho cambio es extremadamente fundamental y rompe con la base de código existente.
fuente
El argumento de que hacer que este comportamiento indefinido permita una mejor optimización no es débil hoy en día. De hecho, es mucho más fuerte hoy que cuando C era nuevo.
Cuando C era nuevo, las máquinas que podían aprovechar esto para una mejor optimización eran en su mayoría modelos teóricos. La gente había hablado sobre la posibilidad de construir CPUs donde el compilador instruiría a la CPU sobre qué instrucciones podrían / deberían ejecutarse en paralelo con otras instrucciones. Señalaron el hecho de que permitir que esto tuviera un comportamiento indefinido significaba que en una CPU de este tipo, si alguna vez existió realmente, podría programar la parte de "incremento" de la instrucción para que se ejecute en paralelo con el resto del flujo de instrucciones. Si bien tenían razón sobre la teoría, en ese momento había poco en el camino del hardware que realmente pudiera aprovechar esta posibilidad.
Eso ya no es solo teórico. Ahora hay hardware en producción y en amplio uso (por ejemplo, Itanium, VLIW DSP) que realmente puede aprovechar esto. Realmente hacen permita que el compilador para generar un flujo de instrucciones que especifica que las instrucciones X, Y y Z pueden todos ser ejecutadas en paralelo. Esto ya no es un modelo teórico: es hardware real en uso real haciendo trabajo real.
En mi opinión, hacer que este comportamiento definido esté cerca de la peor "solución" posible al problema. Claramente no deberías usar expresiones como esta. Para la gran mayoría del código, el comportamiento ideal sería que el compilador simplemente rechazara tales expresiones por completo. En ese momento, los compiladores de C no hicieron el análisis de flujo necesario para detectar eso de manera confiable. Incluso en el momento del estándar C original, todavía no era del todo común.
Tampoco estoy seguro de que sea aceptable para la comunidad hoy en día, aunque muchos compiladores pueden hacer ese tipo de análisis de flujo, generalmente solo lo hacen cuando solicitas la optimización. Dudo que a la mayoría de los programadores les guste la idea de ralentizar las compilaciones de "depuración" solo por el hecho de poder rechazar el código que ellos (estar cuerdos) nunca escribirían en primer lugar.
Lo que C ha hecho es una segunda opción semi-razonable: decirle a la gente que no haga eso, permitiendo (pero no exigiendo) que el compilador rechace el código. Esto evita (aún más) ralentizar la compilación para las personas que nunca lo usarían, pero aún así permite que alguien escriba un compilador que rechace dicho código si quiere (y / o tiene indicadores que lo rechazarán que las personas pueden elegir usar) o no como mejor les parezca).
Al menos IMO, hacer este comportamiento definido sería (al menos cercano a) la peor decisión posible. En el hardware de estilo VLIW, sus opciones serían generar un código más lento para los usos razonables de los operadores de incremento, solo por el mal código que los abusa, o de lo contrario siempre requerirá un análisis de flujo extenso para demostrar que no está tratando con código defectuoso, para que pueda producir el código lento (serializado) solo cuando sea realmente necesario.
En pocas palabras: si desea curar este problema, debe pensar en la dirección opuesta. En lugar de definir qué hace ese código, debe definir el lenguaje para que tales expresiones simplemente no estén permitidas en absoluto (y vivir con el hecho de que la mayoría de los programadores probablemente optarán por una compilación más rápida que hacer cumplir ese requisito).
fuente
a=b[i++];
(por ejemplo) está bien, y optimizarlo es algo bueno. Sin embargo, no veo el punto de dañar un código razonable como ese solo para que algo así++i++
tenga un significado definido.++i++
es precisamente que, en general, es difícil distinguirlas de expresiones válidas con efectos secundarios (comoa=b[i++]
). Puede parecer bastante simple para nosotros, pero si recuerdo el Libro del Dragón correctamente, en realidad es un problema NP-difícil. Es por eso que este comportamiento es UB, en lugar de prohibido.Eric Lippert, diseñador principal del equipo del compilador de C #, publicó en su blog un artículo sobre una serie de consideraciones que se deben tomar para elegir hacer que una característica no esté definida en el nivel de especificación del lenguaje. Obviamente, C # es un lenguaje diferente, con diferentes factores que intervienen en su diseño del lenguaje, pero los puntos que señala son relevantes.
En particular, señala la cuestión de tener compiladores existentes para un lenguaje que tiene implementaciones existentes y también tiene representantes en un comité. No estoy seguro de si ese es el caso aquí, pero tiende a ser relevante para la mayoría de las discusiones de especificaciones relacionadas con C y C ++.
También es de destacar, como usted dijo, el potencial de rendimiento para la optimización del compilador. Si bien es cierto que el rendimiento de las CPU en estos días es mucho mayor de lo que eran cuando C era joven, una gran cantidad de programación C realizada en estos días se realiza específicamente debido a la ganancia potencial de rendimiento y el potencial para (futuro hipotético ) Las optimizaciones de instrucciones de CPU y las optimizaciones de procesamiento multinúcleo serían absurdas debido a un conjunto de reglas demasiado restrictivas para manejar los efectos secundarios y los puntos de secuencia.
fuente
Primero, echemos un vistazo a la definición de comportamiento indefinido:
En otras palabras, "comportamiento indefinido" simplemente significa que el compilador es libre de manejar la situación de la forma que quiera, y cualquier acción de este tipo se considera "correcta".
La raíz del problema en discusión es la siguiente cláusula:
Énfasis añadido.
Dada una expresión como
las subexpresiones
a++
,--b
,c
, y++d
se pueden evaluar en cualquier orden . Además, los efectos secundarios dea++
,--b
y++d
pueden aplicarse en cualquier punto antes del siguiente punto de secuencia (IOW, incluso sia++
se evalúa antes--b
, no se garantiza quea
se actualizará antes de que--b
se evalúe). Como otros han dicho, la razón de este comportamiento es dar a la implementación la libertad de reordenar las operaciones de manera óptima.Debido a esto, sin embargo, expresiones como
etc., arrojará resultados diferentes para diferentes implementaciones (o para la misma implementación con diferentes configuraciones de optimización, o basadas en el código circundante, etc.).
El comportamiento se deja sin definir para que el compilador no tenga la obligación de "hacer lo correcto", sea lo que sea. Los casos anteriores son bastante fáciles de detectar, pero hay un número no trivial de casos que sería difícil o imposible detectar en tiempo de compilación.
Obviamente, puede diseñar un lenguaje de manera que el orden de evaluación y el orden en el que se aplican los efectos secundarios estén estrictamente definidos, y tanto Java como C # lo hacen, en gran medida para evitar los problemas que provocan las definiciones de C y C ++.
Entonces, ¿por qué no se ha realizado este cambio en C después de 3 revisiones estándar? En primer lugar, hay 40 años de código C heredado, y no se garantiza que tal cambio no rompa ese código. Impone un poco de carga a los escritores de compiladores, ya que tal cambio inmediatamente haría que todos los compiladores existentes no sean conformes; todos tendrían que hacer reescrituras significativas. E incluso en CPU rápidas y modernas, aún es posible obtener ganancias de rendimiento reales ajustando el orden de evaluación.
fuente
Primero debe comprender que no solo x = x ++ es indefinido. A nadie le importa x = x ++, ya que no importa cómo lo defina, no tiene sentido. Lo que no está definido es más como "a = b ++ donde a y b son iguales", es decir
Existen varias formas diferentes de implementar la función, dependiendo de lo que sea más eficiente para la arquitectura del procesador (y para las declaraciones que la rodean, en caso de que sea una función más compleja que el ejemplo). Por ejemplo, dos obvios:
o
Tenga en cuenta que el primero que se menciona arriba, el que usa más instrucciones y más registros, es el que necesitaría usar en todos los casos en que ayb no puedan probarse que son diferentes.
fuente
b
antesa
.Legado
La suposición de que C podría reinventarse hoy no puede sostenerse. Hay tantas líneas de códigos C que se han producido y se usan a diario, que cambiar las reglas del juego en el medio del juego es simplemente incorrecto.
Por supuesto, puede inventar un nuevo lenguaje, digamos C + = , con sus reglas. Pero eso no será C.
fuente
Declarar que algo está definido no cambiará los compiladores existentes para respetar su definición. Esto es especialmente cierto en el caso de una suposición en la que se haya podido basar explícita o implícitamente en muchos lugares.
El problema principal para el supuesto no es con
x = x++;
(los compiladores pueden verificarlo fácilmente y deberían advertirlo), es con*p1 = (*p2)++
y equivalente (p1[i] = p2[j]++;
cuando p1 y p2 son parámetros para una función) donde el compilador no puede saber fácilmente sip1 == p2
(en C99restrict
se ha agregado para extender la posibilidad de asumir p1! = p2 entre puntos de secuencia, por lo que se consideró que las posibilidades de optimización eran importantes).fuente
p1[i]=p2[j]++
. Si el compilador puede asumir que no hay alias, no hay problema. Si no puede, debe pasar por el libro - Valor mínimo dep2[j]
primera, almacenarp1[i]
más tarde. Excepto por las oportunidades de optimización perdidas, que no parecen significativas, no veo ningún problema.x = x++;
no ha sido escrito, perot = x; x++; x = t;
nix=x; x++;
o como se quiera como semántica (pero ¿qué pasa con el diagnóstico?). Para un nuevo idioma, simplemente abandone los efectos secundarios.x++
como un punto de secuencia, como si fuera una llamada de función,inc_and_return_old(&x)
sería el truco.En algunos casos, este tipo de código se definió en el nuevo estándar C ++ 11.
fuente
x = ++x
ahora está bien definido (pero nox = x++
)