Compound Literals as Struct Initializers in C
Learn how compound literals work as struct initializers in C, their lifetime rules, and C23 changes. Understand temporary object guarantees.
How do compound literals work as struct initializers in C? Are the temporary objects created guaranteed to be different, and is their lifetime guaranteed to last until the end of the main function?
Consider this example code:
#include <stdint.h>
#include <stdio.h>
struct image {
int width, height;
uint8_t *pixmap;
};
static void iprint(const struct image *img)
{
for (int y = 0; y < img->height; ++y) {
printf("%3i", img->pixmap[y * img->width]);
for (int x = 1; x < img->width; ++x) {
printf(" %3i", img->pixmap[y * img->width + x]);
}
printf("\n");
}
}
static void iflip(struct image *img)
{
for (int y = 0; y < img->height / 2; ++y) {
for (int x = 0; x < img->width; ++x) {
uint8_t tmp = img->pixmap[y * img->width + x];
img->pixmap[y * img->width + x] = img->pixmap[(img->height - 1 - y) * img->width + x];
img->pixmap[(img->height - 1 - y) * img->width + x] = tmp;
}
}
}
int main(void)
{
struct image a = { 3, 2, (uint8_t[]){ 1,2,3,4,5,6 }};
struct image b = { 3, 2, (uint8_t[]){ 1,2,3,4,5,6 }};
iprint(&a);
iprint(&b);
printf("---\n");
iflip(&b);
iprint(&a);
iprint(&b);
}
Bonus question: What changes were made to compound literals in C23, and what was the motivation behind these changes compared to previous versions?
Compound literals in C provide a powerful way to initialize structs with temporary objects using the syntax (type){initializer-list}. These temporary objects are guaranteed to be distinct each time they appear in code, but their lifetime depends on context - they typically last until the end of the block where they’re created, not necessarily until the end of main().
Contents
- What Are Compound Literals in C?
- How Compound Literals Work as Struct Initializers
- Lifetime and Storage Duration of Compound Literals
- Guarantees About Temporary Objects
- C23 Changes to Compound Literals
- Practical Example Analysis
- Best Practices for Using Compound Literals
- Sources
- Conclusion
What Are Compound Literals in C?
A compound literal in C is a language feature that allows you to create an unnamed object of a given type directly within an expression. The syntax looks like a cast but creates a temporary object: (type){initializer-list}. This feature was introduced in the C99 standard and provides a convenient way to create values on-the-fly without declaring named variables.
The GCC documentation explains that “A compound literal looks like a cast containing an initializer. Its value is an object of the type specified in the cast, containing the elements specified in the initializer; it is an lvalue.” This means you can use compound literals wherever you’d use a variable of the same type.
For example, (int[]){1, 2, 3, 4, 5} creates an array of integers with the specified values, while (struct point){x: 10, y: 20} creates a struct point with the given coordinates. These temporary objects can be particularly useful when initializing structs or arrays.
How Compound Literals Work as Struct Initializers
When used as struct initializers, compound literals provide a concise way to populate struct fields without declaring separate variables. In the example code provided, the struct image is initialized using compound literals for the pixmap field:
struct image a = { 3, 2, (uint8_t[]){ 1,2,3,4,5,6 }};
struct image b = { 3, 2, (uint8_t[]){ 1,2,3,4,5,6 }};
Here, (uint8_t[]){ 1,2,3,4,5,6 } creates a temporary array of uint8_t values that becomes the pixmap field for each struct. The SEI CERT C Coding Standard explains that “When it appears in a struct initializer, the literal is evaluated at the point of the initializer and its value is copied into the target field.”
This means the compound literal creates a temporary array, and that array’s contents are copied into the struct’s pixmap field. The temporary array itself then ceases to exist (unless it has static storage duration).
Compound literals work with any struct type, not just those with pointer fields. For example:
struct point { int x; int y; };
struct point p = (struct point){10, 20};
This creates a temporary struct point object with x=10 and y=20, and copies its values into the variable p.
Lifetime and Storage Duration of Compound Literals
The lifetime and storage duration of compound literals depend on where they appear in the code. This is a crucial aspect that developers need to understand to avoid bugs related to dangling pointers.
According to the GCC documentation, “In C, a compound literal designates an unnamed object with static or automatic storage duration.” The actual storage duration is determined by the context:
-
Automatic storage duration (most common): When a compound literal appears inside a block (like in a function), it has automatic storage duration and lasts until the end of that block. This means the temporary object is destroyed when the function exits.
-
Static storage duration: When a compound literal appears at file scope (outside any function) or is explicitly declared with the
statickeyword, it has static storage duration and lasts for the entire program execution.
The SEI CERT C Coding Standard adds that “The temporary object created by the literal has automatic storage duration unless it is explicitly declared static.”
This has important implications for the original question: compound literals used in struct initializers like in the example code typically have automatic storage duration, meaning they don’t last until the end of main() - they last only until the end of the block where they’re created. However, if the struct is initialized at file scope with a static compound literal, it would have static storage duration.
Guarantees About Temporary Objects
The C standard provides important guarantees about compound literals that address the specific questions in the original query:
- Guaranteed distinct objects: The C standard guarantees that each compound literal is a distinct object. As explained in the DEV Community article, “The C standard guarantees that each compound literal is a distinct object. Therefore
a.pixmapandb.pixmappoint to different arrays.”
This means in the example code, the two compound literals (uint8_t[]){ 1,2,3,4,5,6 } create two separate arrays in memory, even though they contain identical values. This is why modifying one doesn’t affect the other, as demonstrated when iflip() is called on struct b but struct a remains unchanged.
- Lifetime guarantees: The lifetime of compound literals with automatic storage duration is guaranteed to last until the end of the block in which they appear. This means in the example code, the compound literals used to initialize a and b would last until the end of main(), but if they were used inside a function, they would only last until that function returned.
The Stack Overflow answer clarifies this: “Inside a block (e.g., int *p = (int[]){1,2,3};) - Automatic storage duration, until the end of the block, each occurrence creates a new object.”
So to directly answer the original question: No, compound literals don’t automatically last until the end of main() - they last only until the end of the block where they’re created. However, in the specific example given, since the compound literals appear in main()'s block scope, they do last until main() exits.
C23 Changes to Compound Literals
C23 introduces significant changes to compound literals that address some limitations of previous versions. According to the analysis from thephd.dev, these changes are motivated by the need for more control over the storage duration and lifetime of compound literals.
The main changes in C23 include:
-
Storage-class specifiers for compound literals: C23 allows explicit storage-class specifiers like
static,thread_local,auto, andconstexprto be used with compound literals. This gives developers more control over the lifetime of these temporary objects. -
constexpr for object definitions: C23 introduces a
constexprstorage-class specifier that allows compound literals to be evaluated at compile time, similar to how it works in C++.
These changes were motivated by several factors:
-
Need for longer lifetimes: In C99 and C11, compound literals had automatic storage duration unless they appeared at file scope. This made it difficult to create compound literals that needed to persist beyond the current block.
-
Consistency with C++: C++ has similar functionality but with different lifetime semantics. The C23 changes bring C more in line with C++ practices.
-
More flexible initialization: The ability to specify storage duration directly in the compound literal syntax makes it easier to write clear, expressive code.
As the thephd.dev article explains, “If you need a longer lifetime, use a storage-class specifier such as static or constexpr (C23).”
For example, in C23 you could write:
struct image a = { 3, 2, static (uint8_t[]){ 1,2,3,4,5,6 }};
This would create a compound literal with static storage duration, ensuring it exists for the entire program execution, not just until the end of the current block.
Practical Example Analysis
Let’s analyze the example code provided to understand how compound literals work in practice:
struct image a = { 3, 2, (uint8_t[]){ 1,2,3,4,5,6 }};
struct image b = { 3, 2, (uint8_t[]){ 1,2,3,4,5,6 }};
In this code:
- Two struct image objects, a and b, are initialized.
- Each has width=3, height=2.
- Each has a pixmap field initialized with a compound literal that creates a temporary array of uint8_t values.
Because compound literals are guaranteed to create distinct objects, a.pixmap and b.pixmap point to separate arrays in memory. When iprint() is called on both structs, they display identical output because their pixmap arrays contain the same values.
After the call to iflip(&b), the pixmap array in struct b is modified, but struct a remains unchanged. This demonstrates that the compound literals created separate, independent arrays.
The lifetime of these compound literals is automatic storage duration, but since they’re created in main()'s block scope, they persist until main() exits. This is why the code works correctly - the temporary arrays don’t disappear before they’re used.
If the compound literals were created in a different function, they would only last until that function returned, potentially leaving the struct with a dangling pointer.
Best Practices for Using Compound Literals
When using compound literals as struct initializers, consider these best practices:
-
Be aware of lifetime issues: Remember that compound literals with automatic storage duration only last until the end of their containing block. If you need the data to persist longer, use static storage duration or copy the data into a longer-lived object.
-
Use static for persistent data: If you need a compound literal to exist for the entire program, use the
staticstorage-class specifier:
static struct image a = { 3, 2, static (uint8_t[]){ 1,2,3,4,5,6 }};
-
Prefer explicit copies for complex structs: For structs with many fields or complex initialization, it might be clearer to declare a separate variable and then copy it into the struct.
-
Consider C23 features: When available, use C23’s enhanced compound literal syntax for better control over storage duration.
-
Don’t rely on pointer equality: Remember that identical compound literals create distinct objects, so pointers to them will not compare equal even if the contents are identical.
-
Use compound literals for simple, temporary data: They’re ideal for one-time initialization where you don’t need to maintain a separate variable.
Sources
- Compound Literals in C - DEV Community
- DCL21-C. Understand the storage of compound literals - SEI CERT C Coding Standard
- Compound Literals - Using the GNU Compiler Collection (GCC)
- c - Compund literals as struct initializers - Stack Overflow
- C23 is Finished: Here is What is on the Menu
Conclusion
Compound literals in C provide a powerful mechanism for creating temporary objects directly in expressions, making them particularly useful for struct initialization. The syntax (type){initializer-list} allows concise initialization without declaring separate variables.
Key takeaways about compound literals as struct initializers:
- They create temporary objects that are guaranteed to be distinct each time they appear
- Their lifetime is typically automatic storage duration, lasting until the end of their containing block
- In the example code, the compound literals last until main() exits because they’re created in main()'s block scope
- C23 introduces enhanced control over storage duration with explicit storage-class specifiers
Understanding these properties is crucial for writing safe, correct C code that doesn’t suffer from dangling pointer issues or unexpected behavior when compound literals are used.
When working with compound literals, always consider where they’re created and how long they need to persist. For data that must outlive its creation context, consider using static storage duration or copying the data into a longer-lived object.