C: asignaciones transitivas (dobles)

He utilizado tal construcción en C:

list->head = list->tail = NULL; 

y ahora considero si esto realmente significa lo que supongo.

¿Esto es malo?

  1. list->head = NULL; list->tail = NULL;

o

  1. list->head = list->tail; list->tail = NULL;

gracias por aclarar

Ninguno de los dos es correcto.

Dado que la asignación simple = operator es asociativa de derecha a izquierda, su expresión es idéntica a:

 list->head = (list->tail = NULL); 

NULL se asigna a tail, y luego a tail, que tiene el valor de un puntero nulo, a la cabeza.

El operador de asignación es asociativo de derecha a izquierda.

Así esta statement de expresión

 list->head = list->tail = NULL; 

es equivalente a

 list->tail = NULL; list->head = list->tail; 

Ninguno de ellos. Significa

 list->tail = NULL; list->head = list->tail; 

1 es incorrecto porque en caso de que el tipo de list->tail no sea un puntero, puede producir un resultado diferente porque el valor se convertirá al tipo del destino de la asignación.

El orden de dos declaraciones en 2 es incorrecto.

Es la asignación múltiple ya que su primera opción es la correcta.

 list->head = list->tail = NULL; 

Es una magia de flujo.

Inicialmente, la tail se establece en NULL , comenzando de derecha a izquierda

 list->tail = NULL; 

entonces list->head = list->tail ;

ahora la tail es NULL por lo que la head también asignará un valor NULL

La asociatividad del operador de asignación = es de derecha a izquierda. Esto significa que si hay más de un operador = en una statement, primero se evalúa el más a la derecha, luego el que queda hacia ella, y en ese orden, hasta el último, se evalúa el operador más a la izquierda.

Esto significa que cuando haces

 list->head = list->tail = NULL; 

la asignación más correcta, es decir, list->tail = NULL se evalúa primero. Así que list->tail será NULL .

Después de eso se evaluará la list->head = list->tail . Y dado que list->tail es NULL (debido a una evaluación previa, es decir, list->tail = NULL ), ahora list->head también es NULL .

Basado en cómo estás representando en tu pregunta, es como

 list->tail = NULL; list->head = list->tail; 

Con todas las respuestas incompletas, tengo que aclarar esto un poco.

Primero, la expresión se evalúa de derecha a izquierda:

 list->head = (list->tail = NULL); 

El estándar define el comportamiento del operador de asignación como:

Un operador de asignación almacena un valor en el objeto designado por el operando izquierdo. Una expresión de asignación tiene el valor del operando izquierdo después de la asignación, 111) pero no es un valor l. El tipo de una expresión de asignación es el tipo que tendría el operando izquierdo después de la conversión de lvalue. El efecto secundario de actualizar el valor almacenado del operando izquierdo se secuencia después de los cálculos de valor de los operandos izquierdo y derecho. Las evaluaciones de los operandos no son posteriores.

El problema es ahora, el estándar deja dos formas de obtener el valor de una asignación. **

 // variant 1 (read the left-hand side after writing it) list->tail = NULL; list->head = list->tail; 

Es decir, list->tail se lee después de que obtuvo su valor y ese valor se asigna a list->head .

 // variant 2 (use temporary storage) typeof(list->tail) temp = NULL; // get the value/type of the RHS of the inner assignment list->tail = temp; list->head = temp; 

(RHS: lado derecho, el término a la derecha de un operador). Gracias, a @chux, las dos tareas también podrían ser intercambiadas.

Ver nota 111 :

Se permite a la implementación leer el objeto para determinar el valor, pero no es obligatorio, incluso cuando el objeto tiene un tipo calificado como volátil .


Las variantes se comportan de manera equivalente con respecto a la máquina abstracta, a menos que list->tail sea ​​calificado volatile (más general: cualquiera, pero el objeto más a la izquierda). Brevemente, volatile le dice al comstackdor que el acceso a un objeto tiene efectos secundarios. Normalmente se usa para registros de hardware periférico, por ejemplo, USART. Si bien rara vez se usa (y si, a menudo, de manera incorrecta) en las aplicaciones de escritorio, por lo general se usa en los controladores de kernel del sistema operativo y en los sistemas incrustados de metal.

Las escrituras y las lecturas de dichos registros suelen pasar los datos escritos al hardware (externo), resp. una lectura produce el valor de dicho registro de hardware. Peor aún, estos registros pueden ser de solo escritura o de solo lectura. Para solo escritura, la lectura produce valores indeterminados que no están relacionados con lo que se escribió. Lo relevante aquí es que los accesos a estos objetos deben ser controlados y secuenciados muy cuidadosamente. No debe haber lectura o escritura inesperada.

Por lo tanto, considere el código anterior. Entonces las variantes generarán diferentes accesos al hardware:

  • La variante 1 resultará en una escritura, seguida de una lectura. Si el registro es de solo escritura, la lectura produce datos no relacionados. Es probable que no sea lo que queremos.
  • La variante 2 solo escribirá, pero reutilizará el valor escrito para la segunda asignación.
  • La variante 2 tiene una secuencia indeterminada del orden en que se realizan las escrituras, que es otra preocupación con volatile valores volatile .

Tenga en cuenta que la variante que se usa no la determina el usuario, sino el comstackdor. Incluso puede variar para diferentes expresiones en el código.

Como consecuencia, las asignaciones encadenadas no son necesarias una vez que se involucra la volatile .