1
Deferred member initialization
www.sandordargo.comAt Meeting C++ 2025, I had an interesting discussion with another attendee. Here’s the problem we talked about: There is a class controlling a piece of hardware to which several other hardware modules could be installed. To manage the available modules, we want to pass a mapping to this class where the map would contain hardware IDs and corresponding module names. A perfect fit for a map! Since the available modules would never change at runtime, the map should ideally be const. However, due to some hard constraints, the map cannot be initialized at construction time. It can happen later through an init function. Let’s explore a few possible solutions—and I’d love to hear your thoughts too. A well-encapsulated non-const member You might argue that there’s no need for _available_modules to be const. As long as it’s private, not exposed through getters, and not modified after initialization, you could rely on discipline and convention. After all, as the owner of the MyHardwareController class, you’ll ensure no code will ever modify it again. 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 // https://godbolt.org/z/Kbx55nn73 #include #include #include #include std::map list_available_modules() { return { {1, "widget"}, {2, "gadget"}, {42, "bar"} }; } class MyHardwareController { public: void init() { _available_modules = list_available_modules(); } std::optional get_module_name(int id) const { if (_available_modules.contains(id)) { return _available_modules.at(id); } return std::nullopt; } private: std::map _available_modules; }; int main() { MyHardwareController bar; bar.init(); for (int id : {1, 2, 3, 42}) { std::cout << "Module " << id << " is named " << bar.get_module_name(id).value_or("") << ".\n"; } return 0; } To make it safer, you can ensure that init() cannot be called twice — either by returning early, signaling an error, or throwing an exception. While some embedded environments avoid exceptions, it’s still worth reconsidering whether that restriction truly applies. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // https://godbolt.org/z/549qqrj9c class MyHardwareController { public: void init() { if (_already_initialized) { throw std::logic_error{"Object already initialized"}; } _available_modules = list_available_modules(); _already_initialized = true; } // ... rest of the class private: bool _already_initialized {false}; std::map _available_modules; }; The downside? Nothing in the type system explicitly communicates that _available_modules should never be modified after initialization. It’s a convention, not an enforced guarantee. Use an optional Instead of a plain, mutable map, we can use an optional. What does this express to the reader? It says: there might or might not be a map yet — but once it exists, it cannot be modified. That’s a strong and useful semantic. You can still replace the entire map using std::optional::emplace(), but the underlying data remains immutable. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // https://godbolt.org/z/hcjsvKnGd class MyHardwareController { public: void init() { if (_already_initialized) { throw std::logic_error{"Object already initialized"}; } _available_modules.emplace(list_available_modules()); _already_initialized = true; } std::optional get_module_name(int id) const { if (_available_modules->contains(id)) { return _available_modules->at(id); } return std::nullopt; } private: bool _already_initialized {false}; std::optional> _available_modules; }; This solution is already quite robust — it enforces immutability once initialized, yet still allows deferred setup. For many cases, it’s more than good enough. Have a registry class But can we communicate our intent even better? Let’s encapsulate the map in a small helper class, ModuleRegistry, responsible for enforcing single initialization and const access. This separation makes the ownership and lifecycle of the data explicit. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class ModuleRegistry { public: void set_once(std::map m) { if (modules) { throw std::logic_error("Modules already initialized"); } modules = std::move(m); } bool is_initialized() const { return modules.has_value(); } const std::map& get_modules() const { if (!modules) { throw std::logic_error("Modules not initialized yet"); } return modules.value(); } private: std::optional> modules; }; Then, our controller simply delegates initialization and access: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class MyHardwareController { public: void init() { if (_module_registry.is_initialized()) { throw std::logic_error{"Module registry already initialized"}; } _module_registry.set_once(list_available_modules()); } std::optional get_module_name(int id) const { if (_module_registry.is_initialized() && _module_registry.get_modules().contains(id)) { return _module_registry.get_modules().at(id); } return std::nullopt; } private: ModuleRegistry _module_registry; }; In this setup, ModuleRegistry fully encapsulates the deferred initialization logic. It cannot be partially modified or reused incorrectly. You can even delete assignment operators to make replacement impossible — just remember the rule of five. See the full example on Compiler Explorer. Conclusion Deferred initialization is a tricky balance between expressiveness, safety, and practicality. If runtime constraints prevent you from initializing const data at construction time, there are still clean ways to express your intent: A private non-const member works but relies on discipline. An optional clearly communicates immutability after initialization. A dedicated registry or wrapper class adds even stronger guarantees and separation of concerns. In the end, the best solution depends on your context. But the key takeaway is this: even when constraints prevent you from using const directly, you can still design for immutability. That mindset — of expressing intent through types - is one of the most powerful tools in modern C++. Connect deeper If you liked this article, please hit on the like button, subscribe to my newsletter and let’s connect on Twitter!
You must log in or # to comment.
