Codementor Events

A Good Use for C's Designated Initializers

Published Mar 25, 2020

Designated initializers are a very useful part of the C programming language, and can lend themselves well to some situations, such as tokenizing and parsing strings of source code.

What are designated initializers?

Designated initializers are a feature of the C programming language which allow you to access and initialize struct elements by name, as well as array elements by their indices.

For example:

struct vec3 {

 float x;
 float y;
 float z;
};

int main(void) {

 struct vec3 v;
 
 v = (struct vec3){ .x = 1.0f, .y = 9.0f, .z = 0.0f };
 
}

Or:

int main(void) {

 char c[6] = {
  [0] = 'h',
  [1] = 'e',
  [2] = 'l',
  [3] = 'l',
  [4] = 'o'
 };
}

Notice that although I declared my array in the second example to contain six members, I only initialized five of them. This is because any uninitialized elements are implicitly initialized to zero, just as one would expect had they instead written:

int main(void) {

 char c[6] = { 'h', 'e', 'l', 'l', 'o' };
}

Why is this useful?

On its own and with just numeric indices, this may not seem all that helpful. But they can be extremely powerful when combined with enumerations. As an example, suppose we are performing tokenizing some sort of source code, and we wish to easily associate a token character or string with a particular enumeration:

enum token_types {

 TK_BREAK,
 TK_CONTINUE,
 TK_DEF,
 TK_RETURN,
 TK_PLUS,
 /* ... */
 TK_EOF,
 TK_INVALID
};

Using this enumeration, we can now order a list of strings without hastle:

static const char* _keywords[] = {

    [TK_CONTINUE] = "continue",
  [TK_BREAK]    = "break",
    [TK_DEF]      = "def",
    [TK_RETURN]   = "return",
    /* ... */
    [TK_INVALID]  = NULL
};

As of C99, designated initializers can appear in any order[1], which means that we can make changes to the underlying enumeration without altering the order of the array of keywords, and we can move the keywords in the array around if we want to as well. Additionally, the array elements which we choose to initialize need not be contiguous -- that is to say, there can be empty spaces. This is excellent, because it allows us to easily separate out code that handles searching for keywords (and by extension, identifiers) from code that handles tokenizing operators, with relative ease.

Now, I can hear you asking:

So why on earth might you do this?

Since I have been careful when crafting my array of keywords (and/or the enumeration which is used to initialize everything), I can use binary search (or linear search if I am not doing many string look-ups like this, and the number of keywords is small) to quickly convert my keywords to enumerations. I can then use those enumeration elsewhere in my code to have constant time indexing for anything else that I need.

However, I feel that the true power lies in associating these enumerations with procedures. As an example, suppose I have produced a list of tokens, and I wish to be able to further perform some syntactic analysis on this list. I can use the previously defined token_type enum to make my life significantly easier:

typedef struct ast_stmt* (stmt_callback)(struct token* tk);

static struct stmt_callback _rules[] = {

 [TK_BREAK]    = parse_break,
 [TK_CONTINUE] = parse_continue,
 [TK_DEF]      = parse_def,
 [TK_RETURN]   = parse_return,
 /* ... */
 [TK_INVALID]  = NULL
};

Thus, indexing into this table becomes as simple as:

/* ... */
struct token* tk = advance();
stmt_callback cb = _rules[tk->type];

if(cb == NULL && tk->type == TK_INVALID) {

 fprintf(stderr, "error[%d:%d]: unexpected token \'%s\'!\n", tk->line, tk->column, tk->value);
 do_error_recovery();
 
 return NULL;
 
}

return (cb == NULL) ? parse_expression_statement(tk) : cb(tk);

/* ... */

Which I would personally say is not all that bad, unless you hate using ternary operators -- in that case, just expand it to an else-if/else and call it a day. However you do it, you still have constant time indexing, and (in my own opinion) a cleaner syntax than cascading if-else statements or a switch block.

Conclusion

Designated initializers can be a valuable tool, and like all things C, can potentially be used to shoot yourself in the foot. I have found that they have made writing code for lexical and syntactic analysis significantly easier, and have reduced the number of changes I need to make when modifying my existing code. Hopefully someone finds this post helpful in some way or another!


  1. This will not work on platforms that do not have support for the C99 (or newer) standard(s). ↩︎

Discover and read more posts from Garrett Bates
get started
post commentsBe the first to share your opinion
Show more replies