Jeremy W. Sherman

stay a while, and listen

OptionBits and BOOL gonna bite you one day

I got to talking with a coworker about some code that tested bitmasks:

1
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:

1
2
3
4
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:

1
2
/* 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:

1
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:

1
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:

1
BOOL is_flag_set = !!(flags & FLAG_A_BIT_TOO_BIG_FOR_BOOL);

You might also see this written like so:

1
2
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.