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

267

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, ies 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, itermina 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+1y el segundo i+2o viceversa; en el segundo, no está claro si ideberí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: ¡ itermina 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.

Nicu Stiurca
fuente
1
Un objeto escalar es un objeto de tipo escalar. Ver 3.9 / 9: "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_ty las versiones calificadas por cv de estos tipos (3.9.3) se denominan colectivamente tipos escalares . "
Rob Kennedy
1
Tal vez hay un error en la página, y en realidad significaban f(i-1, i = -1)o algo similar.
Sr. Lister
Eche un vistazo a esta pregunta: stackoverflow.com/a/4177063/71074
Robert S. Barnes
@RobKennedy Gracias. ¿Los "tipos aritméticos" incluyen bool?
Nicu Stiurca
1
SchighSchagh su actualización debe estar en la sección de respuestas.
Grijesh Chauhan

Respuestas:

343

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:

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 están secuenciadas de forma indeterminada: se pueden realizar en cualquier orden pero no se superponen: o A se completará antes que B o B se completará antes de A. El orden puede ser el opuesto la próxima vez que la misma expresión es evaluado

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:

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.

harmic
fuente
59
Muy buen ejemplo de cómo la expresión podría hacer algo inesperado mientras se ajusta a las reglas de secuencia. Sí, un poco artificial, pero también lo es el código recortado que estoy preguntando en primer lugar. :)
Nicu Stiurca
10
E incluso si la asignación se realiza como una operación atómica, es posible concebir una arquitectura superescalar en la que ambas asignaciones se realicen simultáneamente causando un conflicto de acceso a la memoria que resulte en una falla. El lenguaje está diseñado para que los escritores de compiladores tengan la mayor libertad posible en el uso de las ventajas de la máquina de destino.
ach
11
Realmente me gusta su ejemplo de cómo incluso asignar el mismo valor a la misma variable en ambos parámetros podría dar como resultado un resultado inesperado porque las dos asignaciones no están secuenciadas
Martin J.
1
+ 1e + 6 (ok, +1) para el punto de que el código compilado no siempre es lo que esperarías. Los optimizadores son realmente buenos para tirar este tipo de curvas cuando no sigues las reglas: P
Corey
3
En el procesador Arm, una carga de 32 bits puede tomar hasta 4 instrucciones: lo hace load 8bit immediate and shifthasta 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).
ctrl-alt-delor
208

Primero, "objeto escalar" significa un tipo como a int, floato 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é tarea sucedió "último" i = 1, o i = -1? No está definido en el estándar. Realmente, ese medio ipodría ser 5(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.

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.

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, iaún podría ser 5. 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.

Paul Draper
fuente
9
@hvd, sí, de hecho sé que si habilitas -Wsequence-pointpara g ++, te lo advertirá.
Paul Draper
47
"Estoy seguro de que g ++, Clang y MSVC harán lo que esperabas" No confiaría en un compilador moderno. Son malvados Por ejemplo, podrían reconocer que este es un comportamiento indefinido y asumir que este código es inalcanzable. Si no lo hacen hoy, podrían hacerlo mañana. Cualquier UB es una bomba de tiempo.
CodesInChaos
8
@BlacklightShining "tu respuesta es mala porque no es buena" no es muy útil, ¿verdad?
Vincent van der Weele
13
@BobJarvis Las compilaciones no tienen absolutamente ninguna obligación de generar código incluso remotamente correcto ante un comportamiento indefinido. Incluso puede suponer que este código nunca se llama y, por lo tanto, reemplazar todo con un nop (tenga en cuenta que los compiladores realmente hacen tales suposiciones frente a UB). Por lo tanto, diría que la reacción correcta a dicho informe de error solo se puede "cerrar, funciona según lo previsto"
Grizzly
77
@SchighSchagh A veces, lo que la gente necesita es reformular los términos (que solo en la superficie parece ser una respuesta tautológica). La mayoría de las personas que no conocen las especificaciones técnicas piensan que undefined behaviorsignifica something random will happen, lo que está lejos de ser el caso la mayor parte del tiempo.
Izkata
27

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, esto fue permitido.

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

 #define VALUEB 2

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 Bno están permitidas en algunos idiomas, cuando Bes 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.

Ingo
fuente
Claro, pero el ejemplo hace uso número entero literales. Su 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.
Wolf
3
@Wold Pero el compilador no ve macros de preprocesador. E incluso si esto no fuera así, es difícil encontrar un ejemplo en cualquier lenguaje de programación, donde un código fuente se compila hasta que uno cambia alguna constante int de 1 a 2. Esto es simplemente inaceptable e inexplicable, mientras que aquí se ven muy buenas explicaciones. por qué ese código se rompe incluso con los mismos valores.
Ingo
Sí, las compilaciones no ven macros. Pero, ¿era esta la pregunta?
Wolf
1
Su respuesta está perdiendo el punto, lea la respuesta de harmic y el comentario del OP al respecto.
Lobo
1
Podría hacer 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 escribimos CONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y; ? ¿O no están permitidas las funciones?
Ingo
12

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:

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 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 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.

davidf
fuente
Supongo que en ARM el compilador solo cargará la constante de una tabla. Lo que usted describe parece más como MIPS.
ach
1
@AndreyChernyakhovskiy Sí, pero en un caso cuando no es simplemente -1(que el compilador ha almacenado en alguna parte), sino que es 3^81 mod 2^32constante 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 esperar
yo
@tohecz, sí, ya lo he revisado. De hecho, el compilador es demasiado inteligente para cargar cada constante desde una tabla. De todos modos, nunca usaría el mismo registro para calcular las dos constantes. Esto seguramente 'indefinirá' el comportamiento definido.
ach
@AndreyChernyakhovskiy Pero probablemente no seas "todos los programadores de compiladores de C ++ del mundo". Recuerde que hay máquinas con 3 registros cortos disponibles solo para cálculos.
yo
@tohecz, considere el ejemplo f(i = A, j = B)donde iy json 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 de Ay Ben el mismo registro (como se muestra en la respuesta de @ davidf), porque rompería la semántica del programa.
ach
11

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:

uint8_t v;  // Global

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

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.

Super gato
fuente
9

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.

Amadan
fuente
8

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

3) Cuando se llama a una función (si la función está en línea o no, 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 de postfijo que designa la función llamada, es 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 secuencia 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).
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:

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}
Martin J.
fuente
8

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 se secuencian de forma indeterminada cuando A se secuencia antes de B o B se secuencia antes de A , pero no se especifica cuál. [ Nota : las evaluaciones secuenciadas indeterminadamente no pueden superponerse, pero cualquiera de las dos podría ejecutarse primero. - nota final ]

N5659 § 8.2.2:5
La inicialización de un parámetro, que incluye cada cálculo de valor asociado y efecto secundario, se secuencia indeterminadamente con respecto a 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
AlexD
fuente
2
Gracias por agregar esta actualización para c ++ 17 , así que no tuve que hacerlo. ;)
Yakk - Adam Nevraumont
Impresionante, muchas gracias por esta respuesta. Ligero seguimiento: si fla firma fuera f(int a, int b), ¿C ++ 17 garantiza eso a == -1y b == -2si se llama como en el segundo caso?
Nicu Stiurca
Si. Si tenemos parámetros ay b, entonces, i-entonces- ase inicializan a -1, luego i-entonces- bse inicializan a -2, o al revés. En ambos casos, terminamos con a == -1y b == -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 ".
AlexD
Creo que ha sido lo mismo en C desde siempre.
fuz
5

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

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
JohnB
fuente
1
Es cierto, pero la pregunta era sobre los tipos escalares , que otros han señalado significa esencialmente int familia, familia flotante y punteros.
Nicu Stiurca
El verdadero problema en este caso es que el operador de asignación tiene estado, por lo que incluso la manipulación regular de la variable es propensa a problemas como este.
AJMansfield
2

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 .

Peng Zhang
fuente
Esto se lee un poco como una "respuesta de solo enlace". ¿Puedes copiar los bits relevantes de ese enlace a esta respuesta (en una cita en bloque)?
Cole Johnson
1
@ColeJohnson Esta no es una respuesta de solo enlace. El enlace es solo para una explicación más detallada. Mi respuesta es "puntero", "enum".
Peng Zhang
No dije que tu respuesta fuera solo un enlace. Dije que "se lee como [uno]" . Le sugiero que lea por qué no queremos enlaces solo respuestas en la sección de ayuda. La razón es que, si Microsoft actualiza sus URL en su sitio, ese enlace se rompe.
Cole Johnson
1

En realidad, hay una razón para no depender del hecho de que el compilador verificará que iesté asignado con el mismo valor dos veces, para que sea posible reemplazarlo con una sola asignación. ¿Qué pasa 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)));
}
polkovnikov.ph
fuente
1
No es necesario probar el teorema de Fermat: solo asigna 1a i. O bien ambos argumentos asignan 1y 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.