ASCII-art:
|| a-1 || a || a+1 || ... || b-1 || b || b+1 ||
^^ ^^ ^^ ^^
(i=a) (i=a+1) (i=a+2) ... (i=b)
§ Ruel 4: One always reads loops according to the above rules
for(int i = begin; i != past-the-end; ++i) {
}
§ The rationale for banning relational operators
There is a strong difference in qualia between i < n
and i != n
. The former
makes on aware of when the loop runs; the latter of when the loop quits.
I wish to be cognizant of the precisely when a loop quits.
On writing i != past-the-end
, I know that we quit as soon as we
get past the end . This feels much clearer than being aware that the loops
runs as long as i < n
.
§ half-open indexing: length <-> index
The first advantage of these conventions is that
[begin, past-the-end)
is the same as [begin, begin+length)
. I've found this
to be of great help to flit between length-based-thoughts and
indexing-based-thoughts.
§ half-open indexing: 0
length
The second almost trivial point is that [begin, begin+length)
holds when
length
is zero . That is,
for(int i = begin; i != begin + 0; ++ i) { ... }
does the right thing and iterates over no elements.
§ half-open indexing: -ve
length
The third neat point is that [begin, begin+length)
holds even when length
is negative . Hence, if we want to index the last two elements of an
array, we recall that we start from index (n-1)
, and we want a segment
of length -2
from there, so our loop is:
for(int i = n-1; i != (n-1) + -2; i--) {
}
§ half-open indexing: splitting an array
Finally, this convention helps when attempting to take the beginning part
of a list and the rest of the list. If we want to split an array into
two segments at some index len
,
- The first sub-array is
[0, len)
since it starts at location 0
and has length len
. - Since we have taken
len
elements, the second sub-array must have length n-len
. It must start at index len
since len
is past-the-end for the first sub-array. So, the second sub-array has indeces [len, n-len)
.
Notice how we used both the "length view" and the "past the end" view to
quickly derive the bounds of the second array from the first array.
§ half-open indexing: uniformly generate power-of-2 intervals.
If we want intervals of length 2n: 1, 2, 4, 8, ...
which is common if one
is building data structures such as segment trees and fenwick trees,
in a half-open representation, this literally becomes [a, a+1)
,
[a, a+2)
, [a, a+4)
and so on. On the other hand, if one wants
to use closed interval based indexing, one needs to generate the
series 2n−1, which is [a, a+0]
, [a, a+3]
, [a, a+7]
which is
slightly more finicky.
§ How to deal with strides
If there's a loop
for(int i = 0; i < 5; i += 2) { ... }
the naive i != 5
translation will not work since i
only takes
on even numbers [0, 2, 4, 6, ...]
. For this, we can perform a
simple transformation and always make sure our loop variables increment
by 1
. In a compiler, this is often called as "canonicalizing the loop
induction variable". In LLVM the canonicalization is
performed by the -indvars
pass .
The above example canonicalized becomes:
for(int btc = 0; btc != 5 / 2; btc += 1) { const int i = btc * 2; }
§ In conclusion
These are rules that I dreamed up after noticing my idiosyncracies in
loop-writing.