All code you write is ultimately designed to read and write memory. One aspect of this is the memory model of the language; something which is accompanying us as developers, but we rarely have to think about.
How come, for example, the following code does what we expect it to?
int x = 1;
x = 5;
std::println("{}", x); // prints 5, but why?!
This code prints 5
, but why does it do so?
Intuitively, x = 1
happens first, and then x = 5
stores 5
in the same
variable.
However, who says so?
How come x = 5
isn't executed first?
How come x
can't be 1
or some other value when it gets printed?
In the introductory example, x = 1;
, x += 5
, and std::println("{}", x);
are statements ([stmt.stmt]).
Except as indicated, statements are executed in sequence.
"Executed in sequence" refers to one statement being sequenced before another, which means that it is executed before another by the abstract machine. Furthermore, all three statements are full-expressions:
int x = 1;
is an init-declarator (see 5.4)x = 5;
is an expression which is not a subexpression (see 5.6)- the same as above applies to
std::println("{}", x);
It is important that they are full-expressions, because:
Every value computation and side effect associated with a full-expression is sequenced before every value computation and side effect associated with the next full-expression to be evaluated.
Intuitively, a value computation is a load from memory, such as the one
necessary for x
when printing.
A side effect is (among other things) a write to memory.
For the introductory example, it means that e.g. the side effect of writing
5
to memory during x = 5
is sequenced before the value computation
of x
which takes place during std::println("{}", x)
.
The sequenced before ([intro.execution]) relationship is defined as follows:
Sequenced before is an asymmetric, transitive, pair-wise relation between evaluations executed by a single thread ([intro.multithread]), which induces a partial order among those evaluations. Given any two evaluations A and B, if A is sequenced before B (or, equivalently, B is sequenced after A), then the execution of A shall precede the execution of B. If A is not sequenced before B and B is not sequenced before A, then A and B are unsequenced.
What this means for our example is:
- asymmeric - if
x = 1
is sequenced beforex = 5
, thenx = 5
cannot be sequenced beforex = 1
- transitive - if
x = 1
is sequenced beforex = 5
, andx = 5
is sequenced beforestd::println("{}", x)
, thenx = 1
is sequenced beforestd::println("{}", x)
- irreflexive (not explicitly stated, but implied) -
x = 5
cannot be sequenced before itself
So far, we know that x = 1
is sequenced before x = 5
, and due to
transitivity, this is also sequenced before std::println("{}", x)
.
Neither of these side effects may take place before the value computation,
but it is not yet clear whether 1
, 5
or some other value gets printed,
i.e. which of these side effects is visible.
A visible side effect A on a scalar object or bit-field M with respect to a value computation B of M satisfies the conditions:
- A happens before B and
- there is no other side effect X to M such that A happens before
- X and X happens before B.
The value of a non-atomic scalar object or bit-field M, as determined by evaluation B, shall be the value stored by the visible side effect A.
x = 1
happens before x = 5
,
because it is sequenced before x = 5
(more about happens before later).
x = 1
and x = 5
both happen before std::println("{}", x)
, making both
of them visible according to the first bullet.
However, x = 1
happens before x = 5
, which only makes x = 5
a
visible side effect according to the second bullet.
As a result, the value of x
in std::println("{}", x)
must be 5
, because
storing 5
is the (one and only) visible side effect.
While the three-line example seems simple at first, there is a large amount of rules governing what happens. These rules are fairly intuitive, and mostly boil down to what memory operation is executed first. We can reason about our code by thinking of everything as a sequential execution from top to bottom in our program.