¿Por qué es f(i = -1, i = -1) comportamiento indefinido?


Estaba leyendo sobre el orden de las violaciones 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, ies un objeto escalar , que aparentemente significa

Tipos aritméticos (3.9.1), tipos de enumeración, tipos de puntero, tipos de puntero a miembro (3.9.2), std::nullptr_t y las versiones calificadas para 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 son -1.

¿Puede alguien por favor aclarar?


ACTUALIZACIÓN

Realmente aprecio toda la discusión. Hasta ahora, me gusta @harmic's answer mucho ya que expone las trampas y complejidades de definir esta declaración a pesar de lo sencillo que parece a primera vista. @acheong87 señala algunos problemas que surgen al usar referencias, pero creo que es ortogonal al aspecto de efectos secundarios no secuenciados de esta pregunta.


RESUMEN

Dado que esta pregunta recibió mucha atención, resumiré los principales puntos/respuestas. En primer lugar, permítanme una pequeña digresión para señalar que" por qué " puede tienen significados estrechamente relacionados pero sutilmente diferentes, a saber, " para qué causa", "para qué razón", y "para qué propósito". Agruparé las respuestas por cuál de esos significados de" por qué " abordaron.

Por qué causa

La respuesta principal aquí viene 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 se define 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 debe ser i+1 y el segundo i+2 o viceversa; en el segundo, no está claro si i debe ser 1 o -1 después de la llamada a la función. Ambos casos son UB porque caen bajo los siguientes 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 (IMHO) obvia e inequívoca.

Paul Draper también lo hace explícito en su conclusión que

¿Podría haber sido un comportamiento definido? Sí. Fue definido? No.

Lo que nos lleva a la pregunta de "¿por qué razón/propósito se dejó f(i=-1, i=-1) como comportamiento indefinido?"

Por qué razón / propósito

Aunque hay algunos descuidos (quizás descuidados) en el estándar de C++, muchas omisiones están bien razonadas y sirven para un propósito específico. Aunque soy consciente de que el propósito es a menudo "hacer el trabajo del compilador-escritor más fácil", o" código más rápido", Estaba principalmente interesado en saber si hay una buena razón dejar f(i=-1, i=-1) como UB.

Harmic y supercat proporcionan las principales respuestas que proporcionan una razón para la UB. Harmic señala que un compilador de optimización que podría dividir las operaciones de asignación aparentemente atómicas en múltiples instrucciones de la máquina, y que podría intercalar aún más esas instrucciones para una velocidad óptima. Esto podría llevar a algunos resultados muy sorprendentes: i termina como -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 de conseguir f(i=-1, i=-1) para hacer lo que parece que debe hacer. Señala que en algunas arquitecturas, hay restricciones duras contra múltiples escrituras simultáneas en la misma dirección de memoria. Un compilador podría tener dificultades para captar esto si estuviéramos tratando con algo menos trivial que f(i=-1, i=-1).

Davidf también proporciona un ejemplo de instrucciones de intercalado muy similar a harmic.

Aunque cada uno de los ejemplos de harmic, supercat y davidf son algo inventados, tomados en conjunto, todavía sirven para proporcionar una razón tangible por la que f(i=-1, i=-1) debería ser un comportamiento indefinido.

Acepté la respuesta de Harmic porque hizo el mejor trabajo de abordar todos los significados de por qué, a pesar de que la respuesta de Paul Draper abordó la parte "por qué causa" mejor.

Otras respuestas

JohnB señala que si consideramos operadores de asignación sobrecargados (en lugar de solo escalares simples), entonces también podemos tener problemas.

Author: Community, 2014-02-10

11 answers

Dado que las operaciones no están secuenciadas, no hay nada que decir que las instrucciones que realizan la asignación no puedan intercalarse. Podría ser óptimo hacerlo, dependiendo de la arquitectura de la CPU. La página referenciada indica esto:

Si A no está secuenciado antes de B y B no está secuenciado antes de A, entonces existen dos posibilidades:

  • Las evaluaciones de A y B no están secuenciadas: pueden realizarse en cualquier orden y pueden superponerse (dentro de un solo hilo de ejecución, el el compilador puede intercalar las instrucciones de la CPU que comprenden A y B)

  • Las evaluaciones de A y B tienen una secuencia indeterminada: pueden realizarse en cualquier orden, pero no pueden superponerse: cualquiera de los dos A estará completo antes de B, o B se completará antes de A. La orden puede ser el frente a la próxima vez que se evalúe la misma expresión.

Eso por sí mismo no parece que causaría un problema, suponiendo que la operación sea se realiza almacenando el valor -1 en una ubicación de memoria. Pero tampoco hay nada que decir que el compilador no pueda optimizar eso en un conjunto separado de instrucciones que tenga 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 decrementarla, en comparación con cargar el valor -1. Entonces esto:

f(i=-1, i=-1)

Podría convertirse en:

clear i
clear i
decr i
decr i

Ahora i es -2.

Es probablemente un ejemplo falso, pero es posible.

 331
Author: harmic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-03-24 19:35:37

Primero, "objeto escalar" significa un tipo como a int, float, o un puntero (ver ¿Qué es un objeto escalar en C++?).


Segundo, puede parecer más obvio que {[15]]}

f(++i, ++i);

Tendría un comportamiento indefinido. Pero

f(i = -1, i = -1);

Es menos obvio.

Un ejemplo ligeramente diferente:

int i;
f(i = 1, i = -1);
std::cout << i << "\n";

¿Qué tarea sucedió "última", i = 1, o i = -1? No está definido en el estándar. Realmente, eso significa i podría ser 5 (ver respuesta de harmic para un explicación completamente plausible de cómo este chould ser el caso). O el programa podría segfault. O reformatee su disco duro.

Pero ahora usted pregunta: "¿Qué hay de mi ejemplo? Usé el mismo valor (-1) para ambas asignaciones. ¿Qué podría ser poco claro sobre eso?"

Tienes razón...excepto en la forma en que el comité de estándares de C++ lo describió.

Si un efecto secundario en un objeto escalar no está secuenciado en relación con otro efecto secundario en el mismo escalar objeto, el comportamiento es indefinido.

Ellos podrían haber hecho una excepción especial para tu caso especial, pero no lo hicieron (¿Y por qué deberían hacerlo? ¿De qué serviría eso? Por lo tanto, i todavía podría ser 5. O su disco duro podría estar vacío. Por lo tanto, la respuesta a su pregunta es:

Es un comportamiento indefinido porque no se define lo que es el comportamiento.

(Esto merece énfasis porque muchos programadores piensan " indefinido" significa "aleatorio", o "impredecible". No lo hace; significa no definido por el estándar. El comportamiento podría ser 100% consistente, y aún ser indefinido.)

¿Podría haber sido un comportamiento definido? Sí. Fue definido? No. Por lo tanto, es "indefinido".

Dicho esto, "indefinido" no significa que un compilador formateará su disco duro drive...it 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 servirán lo que esperabas. Simplemente no "tendrían que".


Una pregunta diferente podría ser ¿Por qué el comité de estándares de C++ decidió dejar este efecto secundario sin secuencia?. Esa respuesta incluirá la historia y las opiniones del comité. O ¿Qué tiene de bueno tener este efecto secundario sin secuencia en C++?, que permite cualquier justificación, sea o no el razonamiento real del comité de normas. Usted podría hacer esas preguntas aquí, o en programmers.stackexchange.com.

 207
Author: Paul Draper,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:25

Una razón práctica para no hacer una excepción a las reglas solo porque los dos valores son los mismos:

// config.h
#define VALUEA  1

// defaults.h
#define VALUEB  1

// prog.cpp
f(i = VALUEA, i = VALUEB);

Considere el caso que esto fue permitido.

Ahora, algunos meses después, surge la necesidad de cambiar

 #define VALUEB 2

Aparentemente inofensivo, ¿no? Y sin embargo, de repente prog.cpp no compilaría más. 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 el éxito la compilación depende del valor (más bien del tipo) de una constante.

EDITAR

@HeartWare señaló que las expresiones constantes de la forma A DIV B no están permitidas en algunos idiomas, cuando B 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, desafortunado. Pero ciertamente es bueno restringir tales cosas a lo inevitable.

 25
Author: Ingo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:54:39

El comportamiento se especifica comúnmente como indefinido si hay alguna razón concebible por la que un compilador que estaba tratando de ser "útil" podría hacer algo que causaría un comportamiento totalmente inesperado.

En el caso en el que una variable se escribe varias veces sin nada para garantizar que las escrituras ocurran en momentos distintos, algunos tipos de hardware podrían permitir que se realicen múltiples operaciones de "almacenamiento" simultáneamente a diferentes direcciones utilizando una memoria de doble puerto. Sin embargo, algunos puertos las memorias prohíben expresamente el escenario en el que dos almacenes golpean la misma dirección simultáneamente, independientemente de si los valores escritos coinciden o no. Si un compilador para tal máquina nota dos intentos no secuenciados de escribir la misma variable, puede negarse a compilar o asegurarse de que las dos escrituras no se puedan programar simultáneamente. Pero si uno o ambos de los accesos es a través de un puntero o referencia, el compilador podría no siempre ser capaz de decir si ambas escrituras podrían golpear 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 tal plataforma no sugiere que tal comportamiento no deba definirse en plataformas de hardware cuando se utilizan almacenes de tipos lo suficientemente pequeños como para ser procesados atómicamente. Tratar de almacenar dos valores diferentes de manera no secuenciada podría causar rareza si un compilador no es consciente de ello; para ejemplo, dado:

uint8_t v;  // Global

void hey(uint8_t *p)
{
  moo(v=5, (*p)=6);
  zoo(v);
  zoo(v);
}

Si el compilador alinea la llamada a " moo " y puede decirle que no modifica "v", podría almacenar un 5 a v, luego almacenar un 6 a *p, luego pasar 5 a " zoo", y luego pasar el contenido de v a "zoo". Si " zoo "no modifica" v", no debe haber forma de que las dos llamadas se pasen valores diferentes, pero eso podría suceder fácilmente de todos modos. Por otra parte, en los casos en que ambas tiendas escribirían el mismo valor, tal rareza no podría ocurrir y habría en la mayoría plataformas no ser razón sensata para una implementación para hacer algo raro. 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.

 11
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-26 19:41:09

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 la C está diseñada 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 una int en una variable necesita más de una instrucción. Ejemplo con este pseudo código donde solo se pueden almacenar 8 bits a la vez y debe trabajar en un registro de 32 bits, i es a int32:

reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last

Usted puede imaginar que si el compilador quiere optimizar puede intercalar la misma secuencia dos veces, y no sé qué valor será escrito a i; y digamos que no es muy inteligente:

reg = 0xFF;
reg |= 0xFF00;
reg |= 0xFF0000;
reg = 0xFF;
reg |= 0xFF000000;
i = reg; // writes 0xFF0000FF == -16776961
reg |= 0xFF00;
reg |= 0xFF0000;
reg |= 0xFF000000;
i = reg; // writes 0xFFFFFFFF == -1

Sin embargo, en mis pruebas gcc es lo suficientemente amable como para reconocer que el mismo valor se utiliza dos veces y lo genera una vez y no hace nada extraño. Tengo -1, -1 Pero mi ejemplo sigue siendo válido, ya que es importante considerar que incluso una constante puede no ser tan obvia como parece ser.

 10
Author: davidf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-11 17:25:41

El hecho de que el resultado sería 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 de que ambos valores son -1.

Dado que la expresión se especifica como una con un comportamiento indefinido, un compilador compatible maliciosamente podría mostrar una imagen inapropiada cuando evalúe f(i = -1, i = -1) y cancele la ejecución-y todavía ser considerado completamente correcto. Afortunadamente, no hay compiladores de los que tenga conocimiento que lo hagan.

 9
Author: Amadan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-11 06:40:52

Me parece que la única regla relativa a la secuenciación de la expresión de argumento de función está aquí:

3) Cuando se llama a una función (si la función está o no en línea, y si se usa o no la sintaxis explícita de llamada a función), cada cálculo de valor y efecto secundario asociado con cualquier expresión de argumento, o con la expresión postfijo que designa la función llamada, se secuencian antes de la ejecución de cada expresión o función.

Esto no define la secuenciación entre expresiones de argumento, por lo que terminamos en este caso:

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.

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).
Es, sin embargo, una responsabilidad, ya que depende del comportamiento específico del compilador, incluso si los dos valores asignados son los mismos. Además, obviamente, si intentaras asignar diferentes valores, los resultados serían" verdaderamente " indefinidos:

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}
 8
Author: Martin J.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-10 06:52:03

C++17 define reglas de evaluación más estrictas. En particular, secuencias argumentos de función (aunque en orden no especificado).

N5659 §4.6:15
Evaluaciones Un y B son indeterminately secuenciado cuando sea Un es la secuencia antes de que B o B es la secuencia antes de que Un, pero no se especifica cuál. [Nota : Las evaluaciones secuenciadas indeterminadamente no pueden superponerse, pero tampoco ser ejecutado primero. -nota final ]

N5659 § 8.2.2:5
El la inicialización de un parámetro, incluyendo cada cálculo de valor asociado y efecto secundario, es indeterminada secuenciado con respecto a la de cualquier otro parámetro.

Permite algunos casos que serían UB antes:

f(i = -1, i = -1); // value of i is -1
f(i = -1, i = -2); // value of i is either -1 or -2, but not specified which one
 7
Author: AlexD,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-12 22:17:53

El operador de asignación podría estar sobrecargado, en cuyo caso el orden podría importar:

struct A {
    bool first;
    A () : first (false) {
    }
    const A & operator = (int i) {
        first = !first;
        return * this;
    }
};

void f (A a1, A a2) {
    // ...
}


// ...
A i;
f (i = -1, i = -1);   // the argument evaluated first has ax.first == true
 5
Author: JohnB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-10 14:08:48

Esto es solo responder el "No estoy seguro de lo que "objeto escalar" podría significar además de algo como un int o un float".

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 MSDN de Tipos escalares.

 2
Author: Peng Zhang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-10 06:56:42

En realidad, hay una razón para no depender del hecho de que el compilador comprobará que i está asignado con el mismo valor dos veces, por lo que es posible reemplazarlo con una sola asignación. ¿Y si tenemos algunas expresiones?

void g(int a, int b, int c, int n) {
    int i;
    // hey, compiler has to prove Fermat's theorem now!
    f(i = 1, i = (ipow(a, n) + ipow(b, n) == ipow(c, n)));
}
 1
Author: polkovnikov.ph,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-01 11:38:17