OptionBits and BOOL gonna bite you one day
By: . Published: . Categories: c obj-c pitfalls.I got to talking with a coworker about some code that tested bitmasks:
BOOL isFoo = flags & FLAG_FOO;
Don’t do this; you are inviting pain, suffering, and head-scratching debugging.
I wrote about the wonderland of joy and kittens that is C arithmetic
earlier,
but only in the abstract.
NSUInteger
and BOOL
provide concrete examples that hit where it hurts.
The Trouble
Apple now recommend
you use NSUInteger
for your option bits.
But we keep holding on for dear life to BOOL
, which is a signed char
.
That means our bitmasks and our booleans differ
in both signedness and width.
Have a look-see: Given this seemingly innocuous arrangement:
typedef NS_OPTIONS(NSUInteger, Flags) {
FLAG_A_BIT_TOO_BIG_FOR_BOOL = 0x100
};
NSUInteger flags = FLAG_A_BIT_TOO_BIG_FOR_BOOL;
This naïve flag test assigns zero:
/* DON'T DO THIS! */
BOOL is_flag_set = flags & FLAG_A_BIT_TOO_BIG_FOR_BOOL;
while this works just fine, and gives a non-zero result, as expected:
bool is_flag_set = flags & FLAG_A_BIT_TOO_BIG_FOR_BOOL;
Why _Bool
Is So Swell
The reason is that assignments to _Bool
(which bool
expands to when you include <stdbool.h>
)
are effectively run through a double-bang, as if you’d written this:
bool is_flag_set = !!(flags & FLAG_A_BIT_TOO_BIG_FOR_BOOL);
Aside: Arm64’s BOOL
is _Bool
Added after initial publication. Thanks, Mark!
Running iOS on arm64? Is today ever your lucky day!
Unlike every other Apple platform, arm64 iOS (and the 64-bit simulator)
typedefs BOOL
to be bool
. You get sanity for free.
Just don’t forget to test on a non-arm64 platform if you plan to release to a non-arm64 platform, because it’s still the wild west out there.
Introducing Bang-Bang
The double-bang trick coerces the value to be either 0 or 1:
- The first bang inverts its logical value, so if it was non-zero (true) it’s now 0 (false), and if it was zero (false), it’s now 1 (true).
- The second bang reverses that, and NOT NOT TRUE is just TRUE, so we’re logically back where we started, only now with a tidy, known arithmetic value representing that logical value.
If you apply this trick, then the assignment to BOOL
plays out as you’d hope:
BOOL is_flag_set = !!(flags & FLAG_A_BIT_TOO_BIG_FOR_BOOL);
You might also see this written like so:
BOOL is_flag_set = ((flags & FLAG_A_BIT_TOO_BIG_FOR_BOOL)
== FLAG_A_BIT_TOO_BIG_FOR_BOOL);
Since the result of the bit-and is either 0
or FLAG_A_BIT_TOO_BIG_FOR_BOOL
,
the ==
test results in either 0
or 1
.
There’s Always a Moral
The moral is:
Use
bool
or use!!
, and beware the wicked type conversions.
Get the Gist
You’ll find some ready-to-compile, comment-full sample code demonstrating these issues over in this gist.
*Note: While BOOL
might be signed char
,
YES
and NO
themselves are a bit more than just that now,
to support integer literals. That’s neither here nor there.*
EDIT: Fixed 0 for 1 typo graciously pointed out by Mike Cohen.