How to Create Mutually Referential Static Arrays in C and C++
Defining mutually referential structures is a common pattern in low-level programming. However, things get tricky when you try to define mutually referential static arrays of these structures within the same file. While C handles this with ease, C++ throws compilation errors.
In this article, we'll explore why this difference exists between C and C++, the underlying language standards, and how to write a clean, cross-compatible solution that works in both languages.
The Problem Scenario
Consider two structures, struct a and struct b, that reference each other:
struct a {
struct b *b_ptr;
};
struct b {
struct a *a_ptr;
};If we want to define two static arrays of these structures in a single file where elements of array a point to elements of array b, and vice versa, we might try the following C-style approach:
static struct b b[]; // Forward declaration / tentative definition
static struct a a[] = {
{ &b[17] },
{ &b[23] }
};
static struct b b[] = {
{ &a[7] },
{ &a[11] }
};This compiles perfectly in C using GCC or Clang. However, compiling the exact same code in C++ results in errors like:
error: storage size of ‘b’ isn’t knownor
error: definition of variable with array type needs an explicit size or an initializerWhy Does This Work in C? (Tentative Definitions)
In C, the line static struct b b[]; is known as a tentative definition (C11 §6.9.2). Since it has file scope, no initializer, and is marked static, it acts as a placeholder.
When the compiler encounters &b[17] in the definition of a[], it is able to resolve the address because:
- The element type
struct bis fully defined and its size is known. - The address calculation
&b[17](which is equivalent tob + 17) only requires the size of the element type (sizeof(struct b)), not the size of the entire array.
Later in the file, the actual definition of b[] completes the incomplete array type.
Why Does It Fail in C++?
C++ does not support the concept of "tentative definitions". In C++:
- Every variable declaration with an initializer or without the
externkeyword is a full definition. - The line
static struct b b[];is treated as a definition of an array of unknown bound. In C++, defining a static variable of an incomplete array type without an initializer is ill-formed. - C++ requires the size of an array to be known at its point of definition unless it is immediately initialized (where the compiler infers the size).
The Solution: Writing Cross-Compatible Code
To make this code work in both C and C++, you must provide the compiler with the explicit size of the arrays in their forward declarations. This allows both compilers to allocate the correct storage and perform pointer arithmetic safely.
Approach 1: Forward Declare with Explicit Sizes
By specifying the array sizes upfront, you eliminate the "incomplete type" issue:
#define A_SIZE 10
#define B_SIZE 30
// Forward declarations with explicit sizes
static struct b b[B_SIZE];
static struct a a[A_SIZE] = {
{ &b[17] },
{ &b[23] }
};
// Full definition matching the forward declaration size
static struct b b[B_SIZE] = {
{ &a[7] },
{ &a[9] }
};This approach is fully standard-compliant and compiles seamlessly under both C and C++ compilers.
Approach 2: Use External Linkage (If static is Not Required)
If you do not strictly need the arrays to be static (limited to the file scope), you can use extern declarations. This is widely supported across both languages:
// In a header or at the top of the file
extern struct b b[];
struct a a[] = {
{ &b[17] },
{ &b[23] }
};
struct b b[] = {
{ &a[7] },
{ &a[11] }
};Note that while this works for global/extern arrays, C++ still requires the actual definitions of a[] and b[] to complete the types, which they do at their respective definition lines.
Conclusion
The difference in behavior boils down to C's support for tentative definitions of incomplete array types, a feature intentionally omitted from C++ to enforce stricter type safety and initialization rules. By explicitly defining your array dimensions or utilizing extern declarations, you can easily write clean, robust code that targets both environments without issue.