Hmm, I was reading something recently about mixing levels of abstraction, and how that can lead to hard to read code. Some of the examples involved if statement expressions.
In terms of adding comments to label if/else if branches, I think that would mostly be necessary if you were mixing levels of abstraction. One solution might be to extract the checks into a method, where the name of the method provides a higher level of abstraction, and hence labelling of the if/else if branches.
The main difference between pointers and references are that references are non-nullable and constant. It's a compile error if you try to assign null to a reference. Instead of documenting that a pointer can not accept null, consider using a reference instead. A reference would imply the value needs to point to something valid.
Indeed, you have to go out of your way to trick the compiler into getting a null value stored in a reference variable. One way is to first create a pointer, set it to null, and then create a reference that points to the same thing as the pointer. Failing that, you'd need an ugly cast, or a really awful union, to subvert the type system.
Obj& ref = 0; // Error!
Obj* ptr = 0;
Obj& ref = *ptr; // Okay! (Does not check if ptr is null)
Obj& ref = *(Obj*)0; // Okay! (Casting has subverted the type checks)
union {
Obj* ptr;
// References can not be stored directly in a union, so wrap in a struct
struct Ref {
// Explicitly define a constructor, since otherwise it is deleted due to the reference
Ref();
Obj& ref;
};
} unionVar;
unionVar.ptr = 0; // Okay! (unionVar.ref is now also null)
Another indication you might want to use a reference, is if the pointer itself is constant. References are only ever set when they are defined. They can not be re-assigned. This corresponds to a const pointer (as opposed to a pointer to a const).
Or, put the other way, the two reasons for using a pointer rather than a reference, is to support using null values, or to be able to re-assign the pointer dynamically. If you don't need either feature, I'd recommend using a reference over using a pointer.
It's probably a good idea to use division rather than shift when the actual intent really is to divide. Although easy to forget due to it's prevalence, the shift is an optimization, and one that isn't clear to non-programmers, or people unfamiliar with binary numbers. Using a division really is more clear. Plus, I don't know of any compiler that can't optimize a division by a constant power of 2 into a bit shift.
There is however one gotcha. The rounding mode (towards 0) for signed integers does not match up with a simple bit shift. If you try to divide by a constant power of 2, and only care about positive numbers, but forget to declare the variable as unsigned, you'll get a less efficient opcode sequence with extra instructions to adjust for the rounding mode.
int divideBy2(int value) {
return value / 2;
}
_Z9divideBy2i:
.LFB1564:
.cfi_startproc
mov eax, edi ; Copy the value (we need some scratch space to work with the sign bit)
shr eax, 31 ; Shift right (logical), moving the sign bit to lowest bit position
add eax, edi ; Add 1 (round up towards 0) if value was negative, otherwise add 0 (positive)
sar eax ; Shift right (arithmetic), divide by 2, maintain signdness of result
ret ; Return result (in eax register)
.cfi_endproc
For unsigned values, a rounding mode of towards 0 matches a rounding mode of round down, which is a simple bit shift.
unsigned int divideBy2(unsigned int value) {
return value / 2;
}
_Z9divideBy2j:
.LFB1565:
.cfi_startproc
mov eax, edi ; Copy the value (this could be removed if the method was inlined)
shr eax ; Shift right (logical): Unsigned divide by 2
ret ; Return result (in eax register)
.cfi_endproc