Enforcing that static local variables only exist once

The appeal

Our programs often need resources that should be set up only once and only if they are needed. Commonplace examples include loading large amounts of data into in-memory data structures and initializing third-party libraries. This happens particularly frequently in large, monolithic desktop and server applications that must start quickly and may be terminated before ever needing many of these resources.

The static local variables offered by the C++ programming language are a very appealing solution to this problem. They are initialized the first time control passes through their declaration, and, once initialized, their overhead is often negligible because modern implementations use a double-checked locking pattern that requires only one non-atomic byte comparison to check if the static was already initialized.

The danger

Software evolves over time. Functions grow longer, get split up, get inlined, and their signatures change. Most of the time, code evolution does not need to take any special care about static local variables. However, when the functions that contain these variables become templates that are instantiated more than once, the semantics and the correctness of the program might change. Codebases that often use auto parameters even when the function is only called with a single type are particularly vulnerable to this problem.

This isn’t caused solely by developers not noticing the static local variables. This may also happen because the code isn’t clear about the semantics of these variables.

Making it safe

Documenting that these variables should only exist once in the whole program is as simple and easy as writing a comment. However, compilers don’t read comments, and many programmers behave like compilers. Solutions that are checked by the compiler are always better than solutions subject to human error.

Moving all statics to namespace scope instead of making them local variables is almost never an option, as that would unconditionally initialize all of them before entering main. Making matters worse, the program would likely suffer from the static initialization order fiasco.

We make our compilers check that a given static is instantiated at most once per program for us. This is done through a bit of stateful metaprogramming that uses friend function definitions with different return types to trigger an error if the function containing our detection mechanism gets instantiated twice. We also define a short singleton_static macro that expands to our check followed by the static keyword, which developers can use to not only indicate the desired semantics of their static local variables, but also verify at compile-time that these really are singletons.

template<bool const* p, typename SLOC>
struct assert_single_instantiation final {
	friend consteval std::integral_constant<bool const*, p> detect_multiple_instances(SLOC) {
        return {};
    }
};

#define ASSERT_SINGLE_INSTANTIATION \
	{ \
		static constexpr bool _b = false; \
		[](auto sloc) noexcept { \
			[[maybe_unused]] assert_single_instantiation<&_b, decltype(sloc)> _; \
		}(std::integral_constant<int, __COUNTER__>()); \
	}

#define singleton_static ASSERT_SINGLE_INSTANTIATION; static

In case you were wondering, this does not change the code generated by the compiler in any way, as you can see for yourself in Compiler Explorer.

Making it even better

A shortcoming of this gadget is that the compiler error says that functions that differ only in their return type cannot be overloaded. That’s not a great error message for this use case. Can you write the same check with a static_assert that would allow you to report a custom error message? We would really like to see it.

— by Bernardo Sulzbach

Do you have feedback? Send us a message at devblog@think-cell.com !

Sign up for blog updates

Don't miss out on new posts! Sign up to receive a notification whenever we publish a new article.

Just submit your email address below. Be assured that we will not forward your email address to any third party.

Please refer to our privacy policy on how we protect your personal data.

Share