In C, the initialization of structures can be dangerous if the structure definition ever changes. When a structure is initialized, the order of initializers must follow the structure definition otherwise the members will be assigned the wrong value. This may seem trivial, but take the following example. You have a module which is controlled using a message queue. When the module is first written, the message takes the following simple form.
enum message_id { MSG_ID_READ = 0, MSG_ID_WRITE, MSG_ID_MAX }; struct message { enum message_id id; uint8_t length; };
A helper function is used to build and send the write message. With C90, the function might look like this:
static void send_write_msg(uint16_t length) { struct message msg = {MSG_ID_READ, length}; send_msg(&msg); }
Several years later there is an urgent need to add a priority to the message. Someone comes along and hastily modifies the structure to add the new member.
struct message { enum message_id id; uint8_t priority; uint8_t length; };
But they add it in middle of the structure… The existing initializations will still compile without error since the second member in both the old and new structure definition – priority and length respectively – have the same data type. As long the types match, the compiler has no way of knowing that the structure has changed and is now no longer initialized correctly. Only if the types are different and all the strict compiler checks are enabled will it throw an error if say, a uint32_t is assigned to a uint8_t. Even if existing code does not require this new member, the initialization will still be wrong. Developer testing or a code review might catch this type of error, but it would be even better if it could be avoided all together.
C99 introduced a new feature to address this called designated initializers. It is a way to explicitly initialize a member using a new syntax. The function to send a write message could be instead written to take advantage of this syntax.
void send_write_msg(uint8_t length) { struct message msg = {.id = MSG_ID_READ, .length = length}; send_msg(&msg); }
The member name is prefixed with a period (.) and then assigned the initializer. In the example above, the existing initialization code is still correct after the structure definition was updated. Therefore a structure can be changed without having to update all the initialization code if the new member is not required. So there are two advantages here: 1) it is safer, and 2) it can lead to more efficient development.
This feature also applies to arrays. Let’s say each message has a handler function. If the message IDs are mostly consecutive, the easiest and often most efficient implementation is to define an array of function pointers. A dispatcher then reads the incoming messages and invokes the appropriate handler.
typedef void (*message_handler_t)(uint8_t priority, uint8_t length); static message_handler_t msg_handler[] = { read_handler, write_handler }; ... void message_dispatcher(void) { struct message msg; while (1) { read_msg(&msg); if (msg.id < MSG_ID_MAX) { if (msg_handler[msg.id] != NULL) { msg_handler[msg.id](msg.priority, msg.length); } } } }
Defining the array of message handler seems pretty trivial. However, this is because there are only two messages in this example. If you had 100 messages, keeping track of which handler goes where in the array can be difficult. How do you verify that the handler is at the right index in the array without manually counting? Especially when you may be implementing the handlers out of order. Well, designated initializers in C99 can help here too. Just like with the structures, we can tell the compiler at which index the initializer is intended for.
static message_handler_t msg_handler[] = { [MSG_ID_READ] = read_handler, [MSG_ID_WRITE] = write_handler };
The nice thing about this syntax, is that the array doesn’t have to be initialized in order or even consecutively anymore. And since it explicitly states which index (or in this case message) the initializer is intended for, it makes it easier to read with really big arrays. However you do have to be careful since you can inadvertently create a really big array for no reason. For example, if the first message ID is non-zero and starts at 200 instead – i.e. MSG_ID_READ = 200 – using the ID as a designated initializer will force the compiler to allocate an array of 202 elements, even though the first 200 are unused. This is obviously undesirable, and you might consider using an offset instead of the actual message ID.
Overall, designated initializers in C99 are a definite improvement over C90. Being able to initialize both structures and arrays explicitly can improve the readability and maintainability of your code. And with that, I believe that brings the score to C99:2 – C90:0.