Table of Contents

In this blog post, we’ll explore ways to improve the safety of a simple configuration manager. We’ll handle common pitfalls like dangling references and excessive stack usage. Additionally, we’ll see how C++26 helps enforce safer coding practices with stricter diagnostics and improved handling of large objects.

Let’s go.

Step 1: The Buggy Implementation  

Below is a simple example of a manager object that stores various configs in a map and provides a method to retrieve them. When a requested configuration isn’t found, the code attempts to return a default certificate:

#include <map>
#include <string>
#include <iostream>
#include <vector>
#include <cstdint>

class ConfigManager {
private:
    std::map<std::string, std::vector<uint8_t>> configs;

public:
    ConfigManager() {
        configs["database"] = {
                'h','o','s','t','=','l','o','c','a','l','h','o','s','t',';',
                'p','o','r','t','=','5','4','3','2'
        };
    }

    const std::vector<uint8_t>& findConfig(const std::string& name) const {  
        auto it = configs.find(name);  
        if (it != configs.end()) 
            return it->second; 

        return {42};
    }  

};

int main() {
    ConfigManager configManager;

    const std::vector<uint8_t>& dbConfig = configManager.findConfig("database");  
    
    std::cout << "Database config: ";  
    for (uint8_t byte : dbConfig) {  
        std::cout << static_cast<char>(byte);  
    }  
}

See @Compiler Explorer

Do you see a potential error in this code?

. . .

At first glance, the code looks harmless. However, if the requested entry isn’t found, the function returns a reference to a temporary std::vector. Once the function exits, that temporary is destroyed—leaving you with a dangling reference and undefined behavior.

Compiler Warnings  

Ok, the bug was easy… and compilers have warned about this case for a long time.

While this is helpful, it’s still possible to overlook or disable the warning, especially if your project is large.

So let’s try enabling C++26 mode… for example on GCC 14 you’ll get:

error: returning reference to temporary [-Wreturn-local-addr]

Great!

Now, we cannot skip that error, and we have to fix it.

Step 2: Fixing the Dangling Reference  

One straightforward fix is to ensure that the “default config” has a lifetime extending beyond the function scope. A static object does the trick:

class ConfigManager {
private:
    std::map<std::string, std::vector<uint8_t>> configs;

    static const std::vector<uint8_t>& getDefaultConfig() {
        static std::vector<uint8_t> def {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        return def;
    }
public:
    ConfigManager() {
        configs["database"] = {
                'h','o','s','t','=','l','o','c','a','l','h','o','s','t',';',
                'p','o','r','t','=','5','4','3','2'
        };
    }

    const std::vector<uint8_t>& findConfig(const std::string& name) const {  
        auto it = configs.find(name);  
        if (it != configs.end()) 
            return it->second; 

        return getDefaultConfig();
    }  

};

int main() {
    ConfigManager configManager;

    const std::vector<uint8_t>& dbConfig = configManager.findConfig("database");  
    
    std::cout << "Database config: ";  
    for (uint8_t byte : dbConfig) {  
        std::cout << static_cast<char>(byte);  
    }  
}

See @Compiler Explorer

By returning a static object from getDefaultConfig, we avoid dangling references and undefined behavior. The object’s lifetime is the entire duration of the program, so it’s perfectly safe to return a reference to it.

Just to note: this solution isn’t perfect, and we could explore another approach, such as using std::optional or std::expected. But I’ll leave that for you as a homework assignment :)

The error is now gone, and our program works fine.

But…

Step 3: The Stack Overflow Problem  

Now, consider what happens if requirements change, and the default config has to contain some large data, for example, a binary representation of some image… If it is substantial - say around 1…2MB of data. Storing such a large object in a single vector might look like this:

static const std::vector<uint8_t>& getDefaultConfig() {
    static std::vector<uint8_t> def {
	// 1, 2, 3, ...
	// in total it's 2MB of data
	};
    return def;
}

This seemingly harmless code can risk stack overflow, especially on systems with limited stack space.

Why is that? The object is static, so it shouldn’t take stack’s space…

However, the issue is that we used the initializer list to initialize the vector. Before C++26 the compiler creates a helper array, on the stack, and then copies the data to the vector.

See at C++Insights: https://cppinsights.io/s/85f708c6, the function might be transformed into something like this:

 static inline const std::vector<unsigned char, std::allocator<unsigned char> > & getDefaultConfig()
  {
    const unsigned char __list12_41[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; << helper array!
	...
	

For instance, if you have 4MB of stack space and your default configuration is 1MB, you might occasionally encounter this bug depending on your call stack. This scenario can be quite tricky to catch!

Fortunately, it’s fixed in C++26:

Step 4: The C++26 Solution  

Thanks to “Static storage for braced initializers P2752R3”, in C++26, similar to string literals, arrays for initializer list go into static storage!

What’s more, the fix can be implemented against C++11, so just by compiling with some of the latest compiler, your code might simply get safer.

Conclusion  

In this blog post, we went through a simple scenario where we wrote a buggy code, but thanks to C++26, the code will be safer. The first bug was easy to fix, but it can be missed as current compilers only report warnings. Thanks to C++26, you’ll see a hard compiler error, so you have to address the issue of returning references to temporary objects. But with the fix, we introduced another potential issue with stack overflow. This one is tricky, and even expert coders might not be aware of this subtle issue. But again, thanks to C++26, the bug will disappear with the latest compiler versions.

Back to you

• Have you encountered similar memory management challenges in your C++ projects?
• Are you planning to switch to newer C++ standards to leverage these safety features?

Share your experiences and thoughts in the comments below!