The C language often gives the impression that it can express very limited things. It lacks advanced features like first-class functions and pattern matching. However, C is very simple and still has some very useful syntax tricks and features that not many people know about.
1 Designated Initialization Many people know how to statically initialize an array like this:
int fibs[] = {1, 1, 2, 3, 5};
The C99 standard actually supports a more intuitive and simpler way to initialize various collection types (such as structures, unions, and arrays).2 Arrays We can specify elements of the array for initialization. This is very useful, especially when we need to keep a mapping relationship synchronized based on a set of #define directives. Let’s look at a set of error code definitions, such as:
/* Entries may not correspond to actual numbers. Some entries omitted. */
#define EINVAL 1
#define ENOMEM 2
#define EFAULT 3
/* ... */
#define E2BIG 7
#define EBUSY 8
/* ... */
#define ECHILD 12
/* ... */
Now, suppose we want to provide a string description for each error code. To ensure the array stays updated with the latest definitions, regardless of any modifications or additions made to the header file, we can use this designated syntax for the array.
char *err_strings[] = {[0] = "Success",[EINVAL] = "Invalid argument",[ENOMEM] = "Not enough memory",[EFAULT] = "Bad address",/* ... */[E2BIG ] = "Argument list too long",[EBUSY ] = "Device or resource busy",/* ... */[ECHILD] = "No child processes"/* ... */};
This allows for static allocation of sufficient space, ensuring that the maximum index is valid while initializing special indices to specified values and the remaining indices to 0.3 Structures and Unions Initializing data using the field names of structures and unions is very useful. Suppose we define:
struct point {int x;int y;int z;}
Then, we initialize the struct point like this:
struct point p = {.x = 3, .y = 4, .z = 5};
This method allows for easy generation of the structure at compile time without needing to call a dedicated initialization function when we do not want to initialize all fields to 0. For unions, we can use the same approach, but we only initialize one field.4 Macro Lists A common idiom in C is to have a named list of entities for which functions need to be created to initialize each of them and expand their names in different code modules. This is often used in Mozilla’s source code, and I learned this trick there. For example, in the project I worked on last summer, we had a macro list for tagging each command. It works like this:
#define FLAG_LIST(_) \
_(InWorklist) \
_(EmittedAtUses) \
_(LoopInvariant) \
_(Commutative) \
_(Movable) \
_(Lowered) \
_(Guard)
It defines a FLAG_LIST macro that takes a parameter called _, which itself is a macro that can call each parameter in the list. A practical example might illustrate the issue more intuitively. Suppose we define a macro DEFINE_FLAG, like:
#define DEFINE_FLAG(flag) flag,enum Flag {None = 0,FLAG_LIST(DEFINE_FLAG)Total};#undef DEFINE_FLAG
Expanding FLAG_LIST(DEFINE_FLAG) yields the following code:
enum Flag {None = 0,DEFINE_FLAG(InWorklist)DEFINE_FLAG(EmittedAtUses)DEFINE_FLAG(LoopInvariant)DEFINE_FLAG(Commutative)DEFINE_FLAG(Movable)DEFINE_FLAG(Lowered)DEFINE_FLAG(Guard)Total};
Then, expanding each parameter with the DEFINE_FLAG macro gives us the following enum:
enum Flag {None = 0,InWorklist,EmittedAtUses,LoopInvariant,Commutative,Movable,Lowered,Guard,Total};
Next, we might want to define some accessor functions to better utilize the flag list:
#define FLAG_ACCESSOR(flag) \
bool is##flag() const {\
return hasFlags(1 << flag);\
}\
void set##flag() {\
JS_ASSERT(!hasFlags(1 << flag));\
setFlags(1 << flag);\
}\
void setNot##flag() {\
JS_ASSERT(hasFlags(1 << flag));\
removeFlags(1 << flag);\
}FLAG_LIST(FLAG_ACCESSOR)#undef FLAG_ACCESSOR
Step-by-step demonstration of the process is very enlightening, and if there are any misunderstandings about its usage, one can spend some time on gcc -E.5 Compile-Time Assertions This is actually a very “creative” feature implemented using macros in C. Sometimes, especially in kernel programming, it is very useful to perform conditional checks at compile time rather than at runtime. Unfortunately, the C99 standard does not support any compile-time assertions. However, we can use preprocessing to generate code that will only compile if certain conditions are met (preferably commands that do not perform actual functions). There are various ways to achieve this, usually by creating an array or structure of negative size. The most common way is as follows:
/* Force a compilation error if condition is false, but also produce a result* (of value 0 and type size_t), so it can be used e.g. in a structure* initializer (or wherever else comma expressions aren't permitted). *//* Linux calls these BUILD_BUG_ON_ZERO/_NULL, which is rather misleading. */#define STATIC_ZERO_ASSERT(condition) (sizeof(struct { int:-!(condition); }) )#define STATIC_NULL_ASSERT(condition) ((void *)STATIC_ZERO_ASSERT(condition) )/* Force a compilation error if condition is false */#define STATIC_ASSERT(condition) ((void)STATIC_ZERO_ASSERT(condition))
If the result of (condition) evaluates to a non-zero value (i.e., true in C), then ! (condition) is zero, and the code will compile successfully, generating a zero-sized structure. If (condition) results in 0 (which is false in C), then a compilation error will occur when trying to generate a structure of negative size. Its usage is very simple; if any hypothetical condition can be checked statically, then it can assert at compile time. For example, in the previously mentioned flag list, the type of the flag set is uint32_t, so we can make the following assertion:
STATIC_ASSERT(Total <= 32)
It expands to:
(void)sizeof(struct { int:-!(Total <= 32) })
Now, suppose Total <= 32.Then -!(Total <= 32) equals 0, so this line of code is equivalent to:
(void)sizeof(struct { int: 0 })
This is valid C code.Now suppose there are more than 32 flags, then -!(Total <= 32) equals -1, so this code becomes:
(void)sizeof(struct { int: -1 } )
Since the bit width is negative, it can be determined that if the number of flags exceeds the space we assigned, then the compilation will fail.