C: transitive (double) assignments

后端 未结 6 1163
半阙折子戏
半阙折子戏 2021-01-20 07:11

I have used such construct in C:

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

and now I consider whether this really mean what I suppose.

<
6条回答
  •  傲寒
    傲寒 (楼主)
    2021-01-20 07:30

    With all the incomplete answers, I have to clarify this a bit.

    First, the expression is evaluated right to left:

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

    The standard defines the behaviour of the assignment operator as:

    An assignment operator stores a value in the object designated by the left operand. An assignment expression has the value of the left operand after the assignment,111) but is not an lvalue. The type of an assignment expression is the type the left operand would have after lvalue conversion. The side effect of updating the stored value of the left operand is sequenced after the value computations of the left and right operands. The evaluations of the operands are unsequenced.

    Problem is now, the standard leaves two ways how to get the value of an assignment.**

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

    That is, list->tail is read after it got its value and that value is assigned to 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: right hand side, the term to the right of an operator). Thanks, to @chux, the two assignments could also be swapped.

    See footnote 111:

    The implementation is permitted to read the object to determine the value but is not required to, even when the object has volatile-qualified type.


    The variants behave equivalent with respect of the abstract machine - unless list->tail is qualified volatile (more general: any but the leftmost object). Briefly, volatile tells the compiler the access to an object has side-effects. It is typically used for peripheral hardware registers, e.g. USART. While it is rarely (and if, then often wrongly) used in desktop applications, it is typically used in OS kernel-drivers and bare-metal embedded systems.

    Writes and reads to such registers typically pass the data written to the (external) hardware, resp. a read yields the value of such a hardware register. Worse, these registers can be write-only or read-only. For write-only, reading yields indeterminate values which is not related to what was written. Relevant here is that accesses to these objects have to be very carefully controlled and sequenced. There mut be no unexpected read or write.

    So, consider the code above. Then the variants will generate different accesses to the hardware:

    • Variant 1 will result in a write, followed by a read. If the register is write-only, the read yields unrelated data. That is likely not what we want.
    • Variant 2 will only write, but reuse the value written for the second assignment.
    • Variant 2 has indeterminate sequence of the order the writes are performed, wich is another concern with volatile lvalues.

    Note which variant is used is not detrmined by the user, but the compiler. It might even vary for different expressions in the code.

    As a consequence, chained assignments are a no-go once volatile is involved.

提交回复
热议问题