Check out our Bootcamp Schedule View Schedule

enum-num-num

Mark Dalrymple

As happens occasionally, an interesting technical discussion ensues in an IRC channel. Earlier this week, this question came up why does Apple do this to most of their enums in the Cocoa headers:

enum {
    NSRoundPlain,
    NSRoundDown,
    NSRoundUp,
    NSRoundBankers
};
typedef NSUInteger NSRoundingMode;

Instead of the more straightforward:

typedef enum {
    NSRoundPlain,
    NSRoundDown,
    NSRoundUp,
    NSRoundBankers
} NSRoundingMode;

I can’t speak for Apple’s motivation, but the first thing that jumps to mind is to have variables and struct/object members of this enum type be of a predictable size. The compiler is free to pick any storage size for an enum as it likes, so long as the largest (and most negative) values can be represented in that type.

Edit: A little birdie told me another reason is that the typedef allows for private enum values without getting a type mismatch error from the compiler

The compiler could store an NSRoundingMode in a single byte if it wished, seeing as how the values range from zero to three. Practically, the compiler is going to store enum values in a larger value for efficiency reasons. That’s pretty easy to test:

typedef enum {
    blah1 = 23
} SmallRange1;
...
printf ("sizeof unified: %zun", sizeof(SmallRange1));

Prints out

sizeof unified: 4

This make sense because an int is four bytes in 64-bit land.

Because Apple is using that split enum / typedef convention, the storage is even larger:

enum {
    blah2 = 23
};
typedef NSUInteger SmallRange2;
...
printf ("sizeof split: %zun", sizeof(SmallRange2));

Prints out

sizeof split: 8

This also makes sense because an NSUInteger is eight bytes in 64-bit land.

For the i386 (32-bit Mac) and iOS cases, both of the above enums result in storage of four bytes.

Let’s Get Small

Maybe you want your enums to have as small of a storage as possible. You can can supply the -fshort_enums compiler flag, or turn on “Short Enumeration Constants” in Xcode, to use minimal sizes.

With -fshort_enums, the SmallRange1 enum will only occupy one byte

What happens if somebody modifies the enum?

typedef enum {
    blah1 = 23,
    oopack1 = 16000
} SmallRange1;

The size of SmallRange1 will now require two bytes to hold the new value. If you were depending on the enum being a fixed size, and then suddenly it was larger after someone checked in their header file change, it could cause bugs in you code. As an example, someone is using a packed struct to overlay some data received over the network. If a one-byte interior field of that struct is now a two-byte field, the offsets for the rest of the struct will be in the wrong place.

As mentioned previously, Apple’s LLVM compiler uses an int sized storage. But, in 64-bit land, that means four bytes, not 8. By typedef‘ing the enum to an NSUInteger, it guarantees that it’ll be a 64-bit value.

New Syntax

With Xcode 4.4, the complier has brought some syntax from C++11 into Objective-C that allows you to specify the backing type when you declare the enum. The official verbiage for this is “fixed underlying type.” If you always wanted the NSRoundingMode to take two bytes, you would declare it with the type following the enum keyword. For two bytes, you can use uint16_t, or any other integer type that is 16 bites (such as a short, which is also 16 bits on the Mac and iOS)

typedef enum : uint16_t {
    NSRoundPlain,
    NSRoundDown,
    NSRoundUp,
    NSRoundBankers
} NSRoundingMode;

Now any NSRoundingMode would only take two bytes, even without -fshort-enums. Attempting to stick in a larger value will give you an error. Doing this:

typedef enum : uint16_t {
    NSRoundPlain,
    NSRoundDown,
    NSRoundUp,
    NSRoundBankers,
    NSGronk = 8675309
} NSRoundingMode;

Gives this error:

 error: enumerator value is not representable in the underlying type 'uint16_t'
      (aka 'unsigned short')
    NSGronk = 8675309
    ^

Which to Use?

A second question that came up was, “which version do you use?” If you don’t care about the size of the backing type, just directly typedef the enum. If you do care about the size, like for bit flags, or the enum might be used in a struct or in a public API, then use the fixed underlying type syntax. If that’s not available to you, then use the split enum and typedef.

A third question was “where does it go?”. If it’s part of a public API, such as a public instance variable, or a parameter to a function or method or a return type, the enum will have to go into a header file so other code can include it. If it’s purely an implementation detail, put it in a private header, or directly in the implementation file, so that readers of the header don’t get bogged down in unnecessary implementation details.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project