DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on

Compound Literals in C

#c

Introduction

Among other things, C99 added compound literals for arrays, structs, and unions. Just as 42 specifies an int literal, a compound literal specifies a literal for an array, struct, or union. Consider:

struct point {
  int x, y;
};
typedef struct point point_t;

void draw_circle( point_t center, unsigned radius );
Enter fullscreen mode Exit fullscreen mode

Rather than having to create a temporary object like:

point_t p = { 1, 2 };
draw_circle( p, 5 );
Enter fullscreen mode Exit fullscreen mode

a point_t literal can be specified “inline”:

draw_circle( (point_t){ 1, 2 }, 5 );
Enter fullscreen mode Exit fullscreen mode

That is, you put the type of the literal you want to specify between () and follow that by {} enclosing the type’s value(s). The syntax is similar to a cast, but the result is an lvalue — more later.

The syntax is the same for arrays:

void print_list( char const *list[] );

void f() {
  // ...
  print_list( (char const*[]){ "hello", "world", NULL } );
Enter fullscreen mode Exit fullscreen mode

Specifying Values

Values between {} can be specified the same as always, that is either in declaration order or via designated initializers. For structs or unions, a designated initializer is a . followed by the name of a member:

p = (point_t){ .x = 1, .y = 2 };
Enter fullscreen mode Exit fullscreen mode

For arrays, a designated initializer is an index between []. For example:

int *const a = (int[]){ [3] = 42 };
Enter fullscreen mode Exit fullscreen mode

would create an array [0,0,0,42].

When using designated initializers:

  • For structs or unions, members may be specified in any order; values for members not specified are initialized to 0.
  • For arrays, indices may be specified in any order; values for indices not specified are initialized to 0.

To specify 0 for all values:

  • Use { 0 }, e.g., (point_t){ 0 }.
  • Starting in C23, you can omit the 0, e.g., (point_t){}.

Compound Literals are Lvalues

As mentioned earlier, even though the syntax of compound literals is similar to casts, compound literals are different in a fundamental way: unlike casts, compound literals are lvalues. Briefly:

  • An lvalue is a value, can (but not must) appear on the left-hand-side (hence, L-value) of = (the assignment operator), and can have its address taken via & (the address-of operator).

For compound literals, the important point is that you can take their address via &. For example, if previous draw_circle() function were instead declared as:

void draw_circle( point_t const *center, unsigned radius );
Enter fullscreen mode Exit fullscreen mode

then you could do:

draw_circle( &(point_t){ 1, 2 }, 5 );
Enter fullscreen mode Exit fullscreen mode

that is put & in front. This is quite convenient for large structs: simply pass the pointer instead of copying the entire struct.

Compound Literal Lifetime

For an object that has a name, the lifetime of the object is either forever (if declared at global scope) or is limited to the scope in which it’s declared. It’s the same for compound literals. For example, if you were to do:

point_t *p;
do {
  p = &(point_t){ 1, 2 };
} while (0);              // Lifetime of literal ends here.
printf( "%d\n", p->x );   // Undefined behavior.
Enter fullscreen mode Exit fullscreen mode

Accessing the pointed-to compound literal declared inside the scope via p outside the scope would result in undefined behavior. The lesson is: the lifetime of a compound literal must be at least as long as any pointer to it.

Starting in C23, you can optionally insert a storage class like static:

  p = &(static point_t){ 1, 2 };
Enter fullscreen mode Exit fullscreen mode

that will make it last forever (just like a static local variable inside a function).

For completeness, const, constexpr, register, and thread_local may also be used.

Named Function Arguments

Suppose you have a function that puts some text on the screen in a particular typeface, size, and color at a particular location:

void put_text( char const *text, typeface_t typeface,
               unsigned size, color_t color, point_t loc );

void f() {
  // ...
  put_text( "hello, world!", TYPE_HELVETICA, 12,
            COLOR_BLACK, (point_t){ 10, 50 } );
Enter fullscreen mode Exit fullscreen mode

While calls to such a function are fairly clear, you might occasionally have to look at the function declaration to remember the order in which to specify the arguments. For such functions, it would be nice if you could specify the arguments by name (hence the order wouldn’t matter). But how?

To do this, we can use a struct containing the arguments and change the declaration of put_text() to use it:

struct put_text_args {
  char const *text;
  typeface_t  typeface;
  unsigned    size;
  color_t     color;
  point_t     loc;
};
typedef struct put_text_args put_text_args_t;

void put_text( put_text_args_t const *args );

void f() {
  // ...
  put_text( &(put_text_args_t){
    .text = "hello, world!",
    .typeface = TYPE_HELVETICA,
    .size = 12,
    .color = COLOR_BLACK,
    .loc = (point_t){ 10, 50 }
  } );
Enter fullscreen mode Exit fullscreen mode

Assume that typeface_t and color_t are enumerations. For this example, their definition doesn’t matter.

While that’s better, it’s kind of ugly to have to specify &(put_text_args_t){}. To hide the ugliness, we can use a macro:

#define put_text(...) \
  put_text( &(put_text_args_t){ __VA_ARGS__ } )

void f() {
  // ...
  put_text(
    .text = "hello, world!",
    .typeface = TYPE_HELVETICA,
    .size = 12,
    .color = COLOR_BLACK,
    .loc = (point_t){ 10, 50 }
  );
Enter fullscreen mode Exit fullscreen mode

The name of the macro can match the name of the function and not cause an infinite preprocessor macro expansion loop because the preprocessor will not expand a macro that references itself.

Default Arguments

Now suppose you want to make some of those arguments have default values. It turns out you can specify the same designated initializer more than once and the value of the last one “wins”:

#define put_text_defaults     \
  .typeface = TYPE_HELVETICA, \
  .size = 12,                 \
  .color = COLOR_BLACK

#define put_text(...) \
  put_text( &(put_text_args_t){ put_text_defaults, __VA_ARGS__ } )

void f() {
  // ...
  put_text(
    .text = "hello, world!",
    .size = 24,
    .loc = (point_t){ 10, 50 }
  );
Enter fullscreen mode Exit fullscreen mode

The put_text_defaults macro defines the desired default values that are used in put_text(). If __VA_ARGS__ contains a matching designated initializer such as .size, it will override the value specified by put_text_defaults.

While this works, your compiler will likely give a “initializer overrides prior initialization of this subobject” warning since it usually means you made a mistake. To fix that, we can temporarily disable the warning by using _Pragma:

#ifdef __GNUC__
#define OVERRIDE_ARGS(FN,...)                                     \
  _Pragma( "GCC diagnostic push" )                                \
  _Pragma( "GCC diagnostic ignored \"-Winitializer-overrides\"" ) \
  FN( &(FN##_args_t){ FN##_defaults, __VA_ARGS__ } )              \
  _Pragma( "GCC diagnostic pop" )
#else
// ... OVERRIDE_ARGS() for other compilers ...
#endif

#define put_text(...)  OVERRIDE_ARGS( put_text, __VA_ARGS__ )
Enter fullscreen mode Exit fullscreen mode

Since warnings are compiler-specific, you have to #define OVERRIDE_ARGS() for each compiler you plan to use.

Not in C++

Compound literals are not part of any C++ standard; however, many compilers support them as an extension. Note also that their lifetime is shorter in C++: it extends only until the end of the expression (not scope) in which they’re used. As such, code that would be perfectly OK in C may result in undefined behavior in C++.

Of course, C++ has constructors that can often replace the need for compound literals. My advice is not to use compound literals in C++.

Conclusion

Compound literals are quite handy for creating objects “inline” eliminating what otherwise would have been temporary variables. As shown, they can also be used to enable passing arguments to functions by name.

Top comments (1)

Collapse
 
anmolbaranwal profile image
Anmol Baranwal • Edited

I only learned C++ 🤣