53

Yo solía pensar que en C99, aunque los efectos secundarios de las funciones f y g interfirieron, y aunque la expresión f() + g() no contiene un punto de secuencia, f y g contendría alguna, por lo que el comportamiento sería no especificado: o f() se llamaría antes de g(), o g() antes de f().En C99, ¿está indefinido o simplemente no especificado f() + g()?

Ya no estoy tan seguro. ¿Qué sucede si el compilador especifica las funciones (que el compilador puede decidir incluso si las funciones no están declaradas inline) y luego reordena las instrucciones? ¿Puede uno obtener un resultado diferente de los dos anteriores? En otras palabras, ¿es este comportamiento indefinido?

Esto no es porque tengo la intención de escribir este tipo de cosas, esto es para elegir la mejor etiqueta para una declaración de este tipo en un analizador estático.

+9

+1 buen tema .. –

+0

6.5.2.2 párrafo 12 contiene el ejemplo '(* pf [f1()]) (f2(), f3() + f4())'. Si solo dijera que los efectos colaterales en 'f3' y' f4' interferían, tendría mi respuesta, pero se enfoca más en el hecho de que todos los efectos secundarios están terminados antes de '(* pf [f1()]) 'se llama. –

+0

¿Realmente importa cuál? Cualquiera de los dos significa que no puede confiar en un comportamiento conocido que funciona en FooOS con la versión X.Y.ZpW de BarCC si cambia Foo, Bar, X, Y, Z o W. Lo mejor que puedes esperar es consistencia siempre y cuando te apegues al entorno rígidamente especificado. – dmckee

Respuesta

25

La expresión f() + g() contiene un mínimo de 4 puntos de secuencia; uno antes de la llamada al f() (después de que se evalúen todos los cero argumentos); uno antes de la llamada al g() (después de que se evalúen todos los cero argumentos); uno como la llamada a f() devuelve; y uno como la llamada a g() regresa. Además, los dos puntos de secuencia asociados con f() ocurren tanto antes como después de los dos puntos de secuencia asociados con g(). Lo que no puede decir es en qué orden ocurrirán los puntos de secuencia, si los puntos f ocurren antes de los puntos g o viceversa.

Incluso si el compilador introdujo el código, debe obedecer la regla 'como si' - el código debe comportarse igual que si las funciones no estuvieran entrelazadas. Eso limita el alcance del daño (suponiendo un compilador sin errores).

Por lo tanto, no se especifica la secuencia en la que se evalúan f() y g(). Pero todo lo demás está bastante limpio.


En un comentario, supercat pregunta:

Yo esperaría que las llamadas de función en el código fuente se mantienen como secuencia de puntos, incluso si un compilador decide por sí solo para inline ellos. ¿Eso sigue siendo cierto para las funciones declaradas "en línea", o el compilador obtiene latitud adicional?

creo que el 'como si' regla se aplica y el compilador no consigue latitud adicional para omitir puntos de secuencia, ya que utiliza una función inline explícitamente. La razón principal para pensar que (siendo demasiado perezoso para buscar la redacción exacta en el estándar) es que al compilador se le permite alinear o no una función en línea de acuerdo con sus reglas, pero el comportamiento del programa no debe cambiar (a excepción de actuación).

Además, ¿qué se puede decir sobre la secuenciación de (a(),b()) + (c(),d())? ¿Es posible para c() y/o d() ejecutar entre a() y b(), o para a() o b() para ejecutar entre c() y d()?

  • Claramente, una se ejecuta antes de b, y c se ejecuta antes de d. Creo que es posible que cyd se ejecute entre ayb, aunque es bastante improbable que el compilador genere el código de esa manera; de forma similar, a y b podrían ejecutarse entre c y d. Y aunque he usado 'y' en 'C y D', que podría ser un 'o' - es decir, cualquiera de estas secuencias de operación cumplen con las restricciones:

    • Definitivamente permitido
    • ABCD
    • CDAB
    • permitido Posiblemente (conserva un ≺ b, c ≺ d de pedido)
    • acbd
    • acdb
    • CADB
    • CABD

     
    creo que cubre todas las secuencias posibles. Vea también el chat between Jonathan Leffler and AnArrayOfFunctions - la esencia es que AnArrayOfFunctions no cree que las secuencias 'posiblemente permitidas' estén permitidas en absoluto.

Si tal cosa sería posible, eso implicaría una diferencia significativa entre las funciones en línea y macros.

Existen diferencias significativas entre las funciones en línea y las macros, pero no creo que el orden en la expresión sea una de ellas. Es decir, cualquiera de las funciones a, b, c o d podría reemplazarse por una macro, y podría ocurrir la misma secuencia de macrocuerpos. La principal diferencia, me parece, es que con las funciones en línea, hay puntos de secuencia garantizados en las llamadas a funciones, como se indica en la respuesta principal, así como en los operadores de coma. Con las macros, pierde los puntos de secuencia relacionados con la función. (Entonces, tal vez esa es una diferencia significativa ...) Sin embargo, en muchos sentidos, el problema es más bien como preguntas sobre cuántos ángeles pueden bailar en la punta de un alfiler, no es muy importante en la práctica. Si alguien me presentó con la expresión (a(),b()) + (c(),d()) en una revisión de código, les diría que volver a escribir el código para que quede claro:

a(); 
c(); 
x = b() + d(); 

Y que asume que no hay requisito de secuenciación crítica sobre b() vs d().

+1

Esperaría que las llamadas de función en el código fuente permanezcan como puntos de secuencia, incluso si un compilador decide por sí mismo alinearlos. ¿Eso sigue siendo cierto para las funciones declaradas "en línea", o el compilador obtiene latitud adicional? Además, ¿qué se puede decir acerca de la secuencia de (a(), b()) + (c(), d())? ¿Es posible que c() y/o d() ejecuten entre a() y b(), o para a() o b() para ejecutar entre c() y d()? Si tal cosa fuera posible, eso implicaría una diferencia significativa entre las funciones en línea y las macros. – supercat

+0

@supercat: como dice Jonathan, un orden de ejecución como 'acdb' o' cabd' es posible si 'a()' etc. son funciones en línea o macros. Un ejemplo de la diferencia es que si 'x' es una variable global y tenemos' #define a() x ++ 'y' #define c() x ++ ', luego' (a(), b()) + (c(), d()) 'causará que UB:' x' termine como algo, pero lo más plausible se incrementará una o dos veces dependiendo de cómo se intercalen las instrucciones de lectura-modificación-escritura; mientras que si tenemos 'void a() {x ++; } void c() {x ++; } '(opcionalmente' en línea'), no hay UB y 'x' definitivamente se incrementará dos veces. –

+0

@j_random_hacker: Ciertamente, si a() yc() son macros, su expansión no representa un punto de secuencia o una relación de secuencia. Mi pregunta era sobre las funciones declaradas en línea. Si un compilador decide alinear una función que no está declarada en línea, esperaría que se requiera preservar la semántica de una llamada a función ordinaria. Si una función se declara en línea, ¿eso alivia los requisitos semánticos? – supercat

14

Consulte en el Anexo C una lista de los puntos de secuencia. Las llamadas a funciones (el punto entre todos los argumentos que se evalúan y la ejecución que pasa a la función) son puntos de secuencia. Como ha dicho, no se especifica qué función se llama primero, pero cada una de las dos funciones verá todos los efectos secundarios de la otra o ninguna.

1

@dmckee

Bueno, que no quepa dentro de un comentario, pero aquí es la cosa:

En primer lugar, se escribe un analizador estático correcta. "Correcto", en este contexto, significa que no permanecerá en silencio si hay algo dudoso sobre el código analizado, por lo que en esta etapa confunde alegremente conductas indefinidas y no especificadas. Ambos son malos e inaceptables en el código crítico, y adviertes, con razón, para los dos.

Pero solo quiere advertir una vez por un posible error, y también sabe que su analizador será juzgado en puntos de referencia en términos de "precisión" y "recuperación" en comparación con otros analizadores posiblemente no correctos, por lo no debe advertir dos veces acerca de un mismo problema ... Sea una alarma verdadera o falsa (usted no sabe cuál. Nunca se sabe cuál, de lo contrario sería demasiado fácil).

Así que quieres emitir una única advertencia para

*p = x; 
y = *p; 

Porque tan pronto como p es un puntero válido en la primera declaración, se puede suponer que un puntero válido en la segunda declaración. Y no inferir esto reducirá su puntaje en la métrica de precisión.

Así que le enseña a su analizador que asume que p es un puntero válido tan pronto como lo haya advertido la primera vez en el código anterior, para que no lo advierta la segunda vez. De manera más general, aprendes a ignorar los valores (y las rutas de ejecución) que corresponden a algo que ya has advertido.

Luego, se da cuenta de que no muchas personas escriben código crítico, por lo que realiza otros análisis livianos para el resto de ellos, según los resultados del análisis inicial correcto. Digamos, un cortador de programas en C.

Y les dice "ellos": no tiene que verificar todas las alarmas (posiblemente falsas) emitidas por el primer análisis. El programa rebanado se comporta igual que el programa original, siempre que ninguno de ellos se active. El slicer produce programas que son equivalentes para el criterio de segmentación para rutas de ejecución "definidas".

Y los usuarios ignoran alegremente las alarmas y usan el slicer.

Y luego se da cuenta de que tal vez haya un malentendido. Por ejemplo, la mayoría de las implementaciones de memmove (ya sabes, la que maneja bloques superpuestos) en realidad invocan un comportamiento no especificado cuando se llaman con punteros que no apuntan al mismo bloque (comparando direcciones que no apuntan al mismo bloque). Y su analizador ignora ambas rutas de ejecución, porque ambas no están especificadas, pero en realidad ambas rutas de ejecución son equivalentes y todo está bien.

Por lo tanto, no debe haber ningún malentendido sobre el significado de las alarmas, y si se intenta ignorarlas, solo deben excluirse comportamientos indefinidos inequívocos.

Y así es como terminas con un gran interés en distinguir entre el comportamiento no especificado y el comportamiento indefinido. Nadie puede culparte por ignorar lo último. Pero los programadores escribirán lo primero sin siquiera pensarlo, y cuando diga que su rebanadora excluye los "comportamientos incorrectos" del programa, no lo sentirán como ellos.

Y este es el final de una historia que definitivamente no cabe en un comentario. Disculpas a cualquiera que haya leído eso.

+0

Como alguien que escribe código crítico, dicho analizador proporcionaría más beneficios si me informara sobre el rastro completo de la invalidez de '* p', es decir, una vez que sepa que está mal, no ignore la ruta, haga es todo el mismo problema y dime dónde comienza y la inmensidad de su inducción al error. –

+1

@Mark Esto implica un conjunto completo de otras técnicas (específicamente, hacia atrás), me temo: "¿Qué entradas pueden llevar a que' p' sea inválido aquí? ". "Todo a su tiempo" es la única respuesta que me temo que puedo brindar en este momento. –

+0

Es comprensible que esto sea difícil, pero lo que sugiero es que continúe propagando el error hacia adelante y lo marque como el * mismo * error, en lugar de uno nuevo o no lo haga en absoluto. –

Cuestiones relacionadas