No Missing Field Initializers

Introduction

Recently, after I extended an interface, more specifically a struct, in a C++ program, I encountered some weird bugs. I tried to make my update backward compatible, so I did not quite understand why the bugs would happen.

It turned out that it was due to the new attribute that I added to the struct object was not initialized and it was extremely difficult to root cause this in an extremely large codebase. In retrospect, if the struct object was initialized at construction and the compiler warnings were enabled, I would have caught this bug early at the beginning of my development.

In this blog post, I will discuss the importance of initializing the struct object at construction and enabling the compiler warnings to catch the bugs early using a simplified toy example.

No Missing Field Initializers

This program is bug-free at runtime before the upgrade. After the upgrade, the program becomes problematic at runtime because the value of the new attribute variable is undefined.

The original program initializes the Date object after the construction in the create_random_valid_date function. If the Date object is initialized at construction, with appropriate compiler warnings enabled, the compiler can catch the bug early.

partial_initialization.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>
#include <random>

struct Date
{
int year;
int month;
#if defined(UPGRADED)
// Added a new attribute.
// The struct remains backward compatible.
int day;
#endif
};

// This function remains unchanged.
Date create_random_valid_date()
{
unsigned long long const seed{0};
std::mt19937 gen(seed);
std::uniform_int_distribution<int> year_dist(0, 2024);
std::uniform_int_distribution<int> month_dist(1, 12);
#if defined(INITIALIZE_AT_CONSTRUCTION)
Date const date{.year = year_dist(gen), .month = month_dist(gen)};
#elif defined(INITIALIZE_AFTER_CONSTRUCTION)
Date date;
date.year = year_dist(gen);
date.month = month_dist(gen);
#else
static_assert(false);
#endif
return date;
}

// This function remains unchanged.
void do_some_work_for_date(Date const& date) { static_cast<void>(date); }

#if defined(UPGRADED)
// Added a new function to work with the updated struct.
void do_some_new_work_for_date(Date const& date)
{
// Some undefined behavior will happen if the date, especially the date.day,
// is not valid. For example,
if (date.day < 1)
{
// Crash the program if the date is not valid.
std::terminate();
}
}
#endif

int main()
{
Date const valid_date{create_random_valid_date()};
do_some_work_for_date(valid_date);

#if defined(UPGRADED)
// Running this new function will crash the program if the date is not
// valid. But I did not understand why the date generated from the
// create_random_valid_date function is not valid. It turns out that it was
// due to the new attribute day was not initialized. There is no way for the
// create_random_valid_date function to initialize an updated struct. It is
// also not always possible for me to realize that I need to update the
// create_random_valid_date function in order to upgrade the struct.
// However, it is possible to capture this oversight by enabling the
// compiler warnings.
do_some_new_work_for_date(valid_date);
#endif
}

Original Software

To compile the program before the upgrade, we will define the macro INITIALIZE_AFTER_CONSTRUCTION to GCC.

1
2
$ g++ partial_initialization.cpp -o partial_initialization -DINITIALIZE_AFTER_CONSTRUCTION
$ ./partial_initialization

Notice that in my case, the Date object was initialized after the construction in the create_random_valid_date function. The software is bug-free and runs fine. I did not have to check the implementation of the create_random_valid_date function until I introduced the updates and encountered the bugs.

Upgraded Software

To update the interface, I added a new attribute day to the struct Date. To make it backward compatible as much as possible, I specifically add it to the end of the struct. With the new struct, specifically, the new attribute day, I can start to do some new work with a Date object.

To compile the program after the upgrade, we will define the macro UPGRADED to GCC.

1
2
3
4
$ g++ partial_initialization.cpp -o partial_initialization -DINITIALIZE_AFTER_CONSTRUCTION -DUPGRADED
$ ./partial_initialization
terminate called without an active exception
Aborted (core dumped)

When I ran the program, it crashed. I did not understand why there will be bugs in my program, since I extended the interface struct in a way that is backward compatible on purpose. Because a valid date object was generated from the create_random_valid_date function, I did not expect that the date object would be invalid. However, because the create_random_valid_date function was not updated when the Date struct was updated, the new attribute day was not possible to be initialized. Therefore, in this case, after I introduced the updates, the value of the new attribute day was not undefined and caused the bugs. It took me a very long time to finally realized that the valid_date object was no longer valid after the updates.

Best Practices

Compiler Warnings

The root cause was that the new attribute day was not initialized. If we enable the compiler warnings, can we catch this bug early? The answer is no, because the Date object in the create_random_valid_date function was initialized after the construction. Therefore, the compiler cannot catch this bug.

1
2
3
4
5
6
$ g++ partial_initialization.cpp -o partial_initialization -DINITIALIZE_AFTER_CONSTRUCTION -Wall -Wextra -Werror
$ ./partial_initialization
$ g++ partial_initialization.cpp -o partial_initialization -DINITIALIZE_AFTER_CONSTRUCTION -DUPGRADED -Wall -Wextra -Werror
$ ./partial_initialization
terminate called without an active exception
Aborted (core dumped)

Initialize at Construction

However, if we initialize the Date object at construction in the create_random_valid_date function, with the compiler warnings enabled, we can catch this bug early.

1
2
3
4
5
6
7
8
$ g++ partial_initialization.cpp -o partial_initialization -DINITIALIZE_AT_CONSTRUCTION -Wall -Wextra -Werror
$ ./partial_initialization
$ $ g++ partial_initialization.cpp -o partial_initialization -DINITIALIZE_AT_CONSTRUCTION -DUPGRADED -Wall -Wextra -Werror
partial_initialization.cpp: In function ‘Date create_random_valid_date()’:
partial_initialization.cpp:23:69: error: missing initializer for member ‘Date::day’ [-Werror=missing-field-initializers]
23 | Date const date{.year = year_dist(gen), .month = month_dist(gen)};
| ^
cc1plus: all warnings being treated as errors

Conclusions

To implement a C/C++ program, it is important to initialize a struct object with no missing field initializers at construction. The compiler warnings should also be enabled to catch the unexpected behaviors , espcially from the code that does not belong to the developer, caused by the update early.

References

Author

Lei Mao

Posted on

09-13-2024

Updated on

09-13-2024

Licensed under


Comments