Is there an explanation for inline operators in "k += c += k += c;"?

An operation like a op= b; is equivalent to a = a op b;. An assignment can be used as statement or as expression, while as expression it yields the assigned value. Your statement ...

k += c += k += c;

... can, since the assignment operator is right-associative, also be written as

k += (c += (k += c));

or (expanded)

k =  k +  (c = c +  (k = k  + c));
     10    →   30    →   10 → 30   // operand evaluation order is from left to right
      |         |        ↓    ↓
      |         ↓   40 ← 10 + 30   // operator evaluation
      ↓   70 ← 30 + 40
80 ← 10 + 70

Where during the whole evaluation the old values of the involved variables are used. This is especially true for the value of k (see my review of the IL below and the link Wai Ha Lee provided). Therefore, you are not getting 70 + 40 (new value of k) = 110, but 70 + 10 (old value of k) = 80.

The point is that (according to the C# spec) "Operands in an expression are evaluated from left to right" (the operands are the variables c and k in our case). This is independent of the operator precedence and associativity which in this case dictate an execution order from right to left. (See comments to Eric Lippert's answer on this page).


Now let's look at the IL. IL assumes a stack based virtual machine, i.e. it does not use registers.

IL_0007: ldloc.0      // k (is 10)
IL_0008: ldloc.1      // c (is 30)
IL_0009: ldloc.0      // k (is 10)
IL_000a: ldloc.1      // c (is 30)

The stack now looks like this (from left to right; top of stack is right)

10 30 10 30

IL_000b: add          // pops the 2 top (right) positions, adds them and pushes the sum back

10 30 40

IL_000c: dup

10 30 40 40

IL_000d: stloc.0      // k <-- 40

10 30 40

IL_000e: add

10 70

IL_000f: dup

10 70 70

IL_0010: stloc.1      // c <-- 70

10 70

IL_0011: add

80

IL_0012: stloc.0      // k <-- 80

Note that IL_000c: dup, IL_000d: stloc.0, i.e. the first assignment to k , could be optimized away. Probably this is done for variables by the jitter when converting IL to machine code.

Note also that all the values required by the calculation are either pushed to the stack before any assignment is made or are calculated from these values. Assigned values (by stloc) are never re-used during this evaluation. stloc pops the top of the stack.


The output of the following console test is (Release mode with optimizations on)

evaluating k (10)
evaluating c (30)
evaluating k (10)
evaluating c (30)
40 assigned to k
70 assigned to c
80 assigned to k

private static int _k = 10;
public static int k
{
    get { Console.WriteLine($"evaluating k ({_k})"); return _k; }
    set { Console.WriteLine($"{value} assigned to k"); _k = value; }
}

private static int _c = 30;
public static int c
{
    get { Console.WriteLine($"evaluating c ({_c})"); return _c; }
    set { Console.WriteLine($"{value} assigned to c"); _c = value; }
}

public static void Test()
{
    k += c += k += c;
}

First off, Henk and Olivier's answers are correct; I want to explain it in a slightly different way. Specifically, I want to address this point you made. You have this set of statements:

int k = 10;
int c = 30;
k += c += k += c;

And you then incorrectly conclude that this should give the same result as this set of statements:

int k = 10;
int c = 30;
k += c;
c += k;
k += c;

It is informative to see how you got that wrong, and how to do it right. The right way to break it down is like this.

First, rewrite the outermost +=

k = k + (c += k += c);

Second, rewrite the outermost +. I hope you agree that x = y + z must always be the same as "evaluate y to a temporary, evaluate z to a temporary, sum the temporaries, assign the sum to x". So let's make that very explicit:

int t1 = k;
int t2 = (c += k += c);
k = t1 + t2;

Make sure that is clear, because this is the step you got wrong. When breaking down complex operations into simpler operation you must make sure that you do so slowly and carefully and do not skip steps. Skipping steps is where we make mistakes.

OK, now break down the assignment to t2, again, slowly and carefully.

int t1 = k;
int t2 = (c = c + (k += c));
k = t1 + t2;

The assignment will assign the same value to t2 as is assigned to c, so let's say that:

int t1 = k;
int t2 = c + (k += c);
c = t2;
k = t1 + t2;

Great. Now break down the second line:

int t1 = k;
int t3 = c;
int t4 = (k += c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

Great, we are making progress. Break down the assignment to t4:

int t1 = k;
int t3 = c;
int t4 = (k = k + c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

Now break down the third line:

int t1 = k;
int t3 = c;
int t4 = k + c;
k = t4;
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

And now we can look at the whole thing:

int k = 10;  // 10
int c = 30;  // 30
int t1 = k;  // 10
int t3 = c;  // 30
int t4 = k + c; // 40
k = t4;         // 40
int t2 = t3 + t4; // 70
c = t2;           // 70
k = t1 + t2;      // 80

So when we are done, k is 80 and c is 70.

Now let's look at how this is implemented in the IL:

int t1 = k;
int t3 = c;  
  is implemented as
ldloc.0      // stack slot 1 is t1
ldloc.1      // stack slot 2 is t3

Now this is a bit tricky:

int t4 = k + c; 
k = t4;         
  is implemented as
ldloc.0      // load k
ldloc.1      // load c
add          // sum them to stack slot 3
dup          // t4 is stack slot 3, and is now equal to the sum
stloc.0      // k is now also equal to the sum

We could have implemented the above as

ldloc.0      // load k
ldloc.1      // load c
add          // sum them
stloc.0      // k is now equal to the sum
ldloc.0      // t4 is now equal to k

but we use the "dup" trick because it makes the code shorter and makes it easier on the jitter, and we get the same result. In general, the C# code generator tries to keep temporaries "ephemeral" on the stack as much as possible. If you find it easier to follow the IL with fewer ephemerals, turn optimizations off, and the code generator will be less aggressive.

We now have to do the same trick to get c:

int t2 = t3 + t4; // 70
c = t2;           // 70
  is implemented as:
add          // t3 and t4 are the top of the stack.
dup          
stloc.1      // again, we do the dup trick to get the sum in 
             // both c and t2, which is stack slot 2.

and finally:

k = t1 + t2;
  is implemented as
add          // stack slots 1 and 2 are t1 and t2.
stloc.0      // Store the sum to k.

Since we do not need the sum for anything else, we do not dup it. The stack is now empty, and we're at the end of the statement.

The moral of the story is: when you are trying to understand a complicated program, always break down operations one at a time. Don't take short cuts; they will lead you astray.