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 no está definido.
// snip f(i = -1, i = -1); // undefined behavior
En este contexto, i
es un objeto escalar , que aparentemente significa
Los tipos aritméticos (3.9.1), los tipos de enumeración, los tipos de puntero, los tipos de puntero a miembros (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 por favor?
Realmente aprecio toda la discusión. Hasta ahora, me gusta mucho la respuesta de @ harmic, ya que expone las trampas y 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.
Dado que 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 por cuál de esos significados de "por qué" abordaron.
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.
En general, la respuesta es 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 se rigen por 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 no está definido.
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 hace explícito en su conclusión que
¿Podría haber sido un comportamiento definido? Si. ¿Estaba 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?"
Aunque hay algunos descuidos (tal vez 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 a menudo es "facilitar el trabajo del compilador-escritor" o "codificar más rápido", me interesaba principalmente saber si hay una buena razón para dejar el puesto 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 podría dividir las operaciones de asignación aparentemente atómicas en múltiples instrucciones de máquina, y que podría intercalar aún más esas instrucciones para una velocidad óptima. Esto podría llevar a 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 nocivos si las operaciones no están secuenciadas.
supercat proporciona una exposición relacionada de las trampas de intentar f(i=-1, i=-1)
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 entrelazadas muy similares a las de harmic.
Aunque cada uno de los ejemplos de harmic, supercat y davidf 'son algo artificiales, 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 al abordar todos los significados del por qué, aunque la respuesta de Paul Draper abordó mejor la parte "por qué causa".
JohnB señala que si consideramos operadores de asignación sobrecargados (en lugar de simples escalares), también podemos tener problemas.
Dado que las operaciones no están secuenciadas, no hay nada que diga que las instrucciones que realizan la asignación no se puedan intercalar. Podría ser óptimo hacerlo, según la arquitectura de la CPU. La página referenciada dice esto:
Si A no se secuencia antes de B y B no se secuencia antes de A, 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 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: o A estará completo antes que B, o B estará completo antes que A. El orden puede ser el opuesto la próxima vez que la misma expresión se evalúa.
Eso por sí solo no parece que cause un problema, asumiendo que la operación que se está realizando 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 tienen el mismo efecto, pero que podrían fallar si la operación se intercala con otra operación en la misma ubicación de memoria.
Por ejemplo, imagine que es más eficiente poner a cero la memoria y luego disminuirla, en comparación con cargar el valor -1 pulg. Entonces esto:
f(i=-1, i=-1)
podría convertirse:
clear i
clear i
decr i
decr i
Ahora tengo -2.
Probablemente sea un ejemplo falso, pero es posible.
En primer lugar, "objeto escalar" significa un tipo como una int
, float
o un puntero (ver ¿Qué es un objeto escalar en C ++? ).
En segundo lugar, puede parecer más obvio que
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é asignación ocurrió "última" i = 1
, o i = -1
? No está definido en el estándar. Realmente, ese medio i
podría ser 5
(consulte 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 reformatea tu disco duro.
Pero ahora pregunta: "¿Qué pasa con mi ejemplo? Usé el mismo valor ( -1
) para ambas asignaciones. ¿Qué podría no estar claro acerca de eso?"
Tiene razón ... excepto en la forma en que el comité de estándares de C ++ describió esto.
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 no está definido.
Ellos podrían haber hecho una excepción especial para su caso especial, pero no lo hicieron. (¿Y por qué habrían de hacerlo? ¿Qué utilidad tendría eso?) Entonces, i
todavía podría serlo 5
. O su disco duro podría estar vacío. Por tanto, la respuesta a su pregunta es:
Es un comportamiento indefinido porque no está definido cuál es el comportamiento.
(Esto merece un énfasis porque muchos programadores piensan que "indefinido" significa "aleatorio" o "impredecible". No es así; 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. ¿Estaba 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á 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 hacerlo".
Una pregunta diferente podría ser ¿Por qué el comité de estándares de C ++ decidió que este efecto secundario no tuviera secuencia? . Esa respuesta involucrará la historia y las opiniones del comité. O ¿Qué tiene de bueno tener este efecto secundario sin secuenciar 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.
Una razón práctica para no hacer una excepción a las reglas solo porque los dos valores son iguales:
// config.h
#define VALUEA 1
// defaults.h
#define VALUEB 1
// prog.cpp
f(i = VALUEA, i = VALUEB);
Considere el caso de que esto estuviera permitido.
Ahora, unos meses después, surge la necesidad de cambiar
#define VALUEB 2
Aparentemente inofensivo, ¿no? Y, sin embargo, de repente, prog.cpp ya no se compilaba. Sin embargo, creemos que la compilación no debería depender del valor de un literal.
En pocas palabras: no hay una excepción a la regla porque haría que la compilación exitosa dependiera del valor (más bien del tipo) de una constante.
¿Por qué f (i = -1, i = -1) es un comportamiento indefinido? que las expresiones constantes del formulario A DIV B
no están permitidas en algunos idiomas, cuando B
es 0, y hacen que la compilación falle. Por tanto, el cambio de una constante podría provocar errores de compilación en otro lugar. Lo cual es, en mi humilde opinión, lamentable. Pero ciertamente es bueno restringir tales cosas a lo inevitable.
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 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 transportar 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 trabajar en un registro de 32 bits, i es un int32:
reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last
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 listo:
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 tiene la amabilidad de 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.
El comportamiento se especifica comúnmente como indefinido si existe 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 que garantice que las escrituras suceden en momentos distintos, algunos tipos de hardware pueden permitir que se realicen varias 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 acceden a la misma dirección simultáneamente, independientemente de si los valores escritos coinciden o no . Si un compilador de una máquina de este tipo nota dos intentos no secuenciados para escribir la misma variable, podría negarse a compilar o asegurarse de que las dos escrituras no se puedan programar simultáneamente. Pero si uno o ambos accesos es a través de un puntero o referencia, es posible que el compilador no siempre pueda decir si ambas escrituras pueden llegar a la misma ubicación de almacenamiento. En ese caso, podría programar las escrituras simultáneamente, provocando 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 procesarse atómicamente. Tratar de almacenar dos valores diferentes de forma no secuenciada podría causar rarezas si un compilador no lo sabe; por ejemplo, dado:
uint8_t v; // Global
void hey(uint8_t *p)
{
moo(v=5, (*p)=6);
zoo(v);
zoo(v);
}
si el compilador en línea la llamada a "moo" y puede decir que no modifica "v", podría almacenar un 5 av, luego almacenar un 6 to * p, luego pasar 5 a "zoo", y luego pasar el contenido 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 los que ambas tiendas escribieran el mismo valor, tal rareza no podría ocurrir y en la mayoría de las plataformas no habría una razón sensata para que una implementación hiciera 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.
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 que cumpla con las normas malintencionadas podría mostrar una imagen inapropiada cuando evalúe f(i = -1, i = -1)
y anule la ejecución, y aún así se considere completamente correcto. Afortunadamente, ningún compilador que yo sepa lo hace.
Me parece que la única regla relacionada con la secuenciación de la expresión del argumento de la función está aquí:
3) Al llamar a una función (si la función está en línea o no, y si se usa o no la sintaxis de llamada de función explícita), cada cálculo de valor y efecto secundario asociado con cualquier expresión de argumento, o con la expresión de sufijo que designa la función llamada, secuenciado antes de la ejecución de cada expresión o declaración en el cuerpo de la función llamada.
Esto no define la secuenciación entre expresiones de argumentos, 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 no está definido.
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 los mismos. Además, obviamente, si intentara asignar valores diferentes, 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();
}
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).
N5659 §4.6:15
Las evaluaciones A y B tienen una secuencia indeterminada cuando A se secuencia antes que B o B se secuencia antes que A , pero no se especifica cuál. [ Nota : Las evaluaciones en secuencia indeterminada no pueden superponerse, pero cualquiera de las dos se podría ejecutar primero. - nota final ]
N5659 § 8.2.2:5
La inicialización de un parámetro, incluido todo cálculo de valor asociado y efecto secundario, se secuencia de forma indeterminada con respecto a la de cualquier otro parámetro.
Permite algunos casos que antes serían UB:
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
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
Esto solo responde al "No estoy seguro de qué podría significar" objeto escalar "además de algo como un int o un flotante".
Yo 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 sobre tipos escalares .
En realidad, hay una razón para no depender del hecho de que el compilador verificará que i
se le asigne el mismo valor dos veces, de modo que sea 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)));
}
La estrella de HGTV, Christina Hall, revela que le diagnosticaron envenenamiento por mercurio y plomo, probablemente debido a su trabajo como manipuladora de casas.
Recientemente salió a la luz un informe policial que acusa a la estrella de 'Love Is Blind', Brennon, de violencia doméstica. Ahora, Brennon ha respondido a los reclamos.
Conozca cómo Wynonna Judd se dio cuenta de que ahora es la matriarca de la familia mientras organizaba la primera celebración de Acción de Gracias desde que murió su madre, Naomi Judd.
Descubra por qué un destacado experto en lenguaje corporal cree que es fácil trazar "tales paralelismos" entre la princesa Kate Middleton y la princesa Diana.
Los inodoros arrojan columnas de aerosol invisibles con cada descarga. ¿Como sabemos? La prueba fue capturada por láseres de alta potencia.
Air travel is far more than getting from point A to point B safely. How much do you know about the million little details that go into flying on airplanes?
The world is a huge place, yet some GeoGuessr players know locations in mere seconds. Are you one of GeoGuessr's gifted elite? Take our quiz to find out!
¿Sigue siendo efectivo ese lote de repelente de insectos que te quedó del verano pasado? Si es así, ¿por cuánto tiempo?
Tapas elásticas de silicona de Tomorrow's Kitchen, paquete de 12 | $14 | Amazonas | Código promocional 20OFFKINJALids son básicamente los calcetines de la cocina; siempre perdiéndose, dejando contenedores huérfanos que nunca podrán volver a cerrarse. Pero, ¿y si sus tapas pudieran estirarse y adaptarse a todos los recipientes, ollas, sartenes e incluso frutas en rodajas grandes que sobran? Nunca más tendrás que preocuparte por perder esa tapa tan específica.
Hemos pirateado algunas ciudades industriales en esta columna, como Los Ángeles y Las Vegas. Ahora es el momento de una ciudad militar-industrial-compleja.
Un minorista está enlatando su sección de tallas grandes. Pero no están tomando la categoría solo en línea o descontinuándola por completo.
Entiendo totalmente, completamente si tienes una relación difícil con los animales de peluche. Son lindos, tienen valor sentimental y es difícil separarse de ellos.
El equipo está a la espera de las medallas que ganó en los Juegos Olímpicos de Invierno de 2022 en Beijing, ya que se está resolviendo un caso de dopaje que involucra a la patinadora artística rusa Kamila Valieva.
Miles de compradores de Amazon recomiendan la funda de almohada de seda Mulberry, y está a la venta en este momento. La funda de almohada de seda viene en varios colores y ayuda a mantener el cabello suave y la piel clara. Compre las fundas de almohada de seda mientras tienen hasta un 46 por ciento de descuento en Amazon
El jueves se presentó una denuncia de delito menor amenazante agravado contra Joe Mixon.
El Departamento de Policía de Lafayette comenzó a investigar a un profesor de la Universidad de Purdue en diciembre después de recibir varias denuncias de un "hombre sospechoso que se acercaba a una mujer".
Al igual que el mundo que nos rodea, el lenguaje siempre está cambiando. Mientras que en eras anteriores los cambios en el idioma ocurrían durante años o incluso décadas, ahora pueden ocurrir en cuestión de días o incluso horas.
Estoy de vuelta por primera vez en seis años. No puedo decirte cuánto tiempo he estado esperando esto.
“And a river went out of Eden to water the garden, and from thence it was parted and became into four heads” Genesis 2:10. ? The heart is located in the middle of the thoracic cavity, pointing eastward.
Creo, un poco tarde en la vida, en dar oportunidades a la gente. Generosamente.