C Compound Literals: Structs and Arrays on the Fly

Compound literals, introduced in C99, provide a mechanism to create unnamed, temporary objects—specifically structures, unions, and arrays—directly within expressions. By combining a type cast with an initializer list, developers can instantiate and populate complex data structures on the fly without having to declare auxiliary variables beforehand. This article explains how compound literals function, breaks down their syntax for structs and arrays, examines their memory lifetimes, and highlights practical use cases like passing dynamic arguments to functions.

Understanding the Syntax

A compound literal consists of a parenthesized type name followed by an initializer list enclosed in braces:

(type_name){ initializer-list }

Unlike a standard cast, which converts an existing value to another type, a compound literal actually constructs a new, unnamed lvalue of the specified type.

Instantiating Unnamed Structs

Before compound literals, passing a populated struct to a function required declaring a named variable, initializing its fields, and passing that variable. With compound literals, this process can be condensed into a single line.

#include <stdio.h>

struct Point {
    int x;
    int y;
};

void print_point(struct Point p) {
    printf("Point: (%d, %d)\n", p.x, p.y);
}

int main(void) {
    // Pass an unnamed struct directly to the function
    print_point((struct Point){ .x = 10, .y = 20 });
    return 0;
}

Designated initializers can be used inside the compound literal to explicitly map values to fields, improving readability and avoiding reliance on field declaration order.

Instantiating Unnamed Arrays

Compound literals apply equally to arrays. When declared, an array compound literal evaluates to a pointer to its first element, allowing immediate use where array decay is expected.

#include <stdio.h>

void print_sum(const int *numbers, size_t count) {
    int sum = 0;
    for (size_t i = 0; i < count; i++) {
        sum += numbers[i];
    }
    printf("Sum: %d\n", sum);
}

int main(void) {
    // Create an unnamed array on the fly and pass it
    print_sum((int[]){ 1, 2, 3, 4, 5 }, 5);
    return 0;
}

Scope and Lifetime Rules

The lifetime of an object created via a compound literal depends on where it is defined:

  • Block Scope: When written inside a function body, the compound literal possesses automatic storage duration. Its memory exists only until the enclosing block terminates. Returning a pointer to a block-scope compound literal leads to undefined behavior.
  • File Scope: When written outside all functions, the compound literal possesses static storage duration, existing for the entire runtime of the program.

You can also create a read-only compound literal by adding the const qualifier:

const int *ptr = (const int[]){ 10, 20, 30 };

Attempting to modify the contents of a const-qualified compound literal produces a compiler error or undefined behavior at runtime.

Key Benefits

  1. Eliminates Boilerplate: Removes the clutter of temporary, single-use variable declarations.
  2. Improves Readability: Keeps data initialization colocated with the function call or assignment where it is needed.
  3. Modifiable Lvalues: Because compound literals are true lvalues (unless declared const), their addresses can be taken using the & operator, and their members can be modified directly.