Detecting multiple instantiations

Detecting multiple instantiations

In the previous post 'Enforcing that static local variables only exist once' we discovered a way to enforce, at compile time, that a piece of code should only be instantiated once. In summary, for each instantiation of the code in question, we declare a function with different a return type. If you try to instantiate the code, then compilation would fail with the following error:

error: functions that differ only in their return type cannot be overloaded

As clever as this solution is, it has a few issues:

  1. The error message is not great.
  2. When instantiating from different translation units, the compilation would not fail.

It can be argued that the first point is merely an inconvenience. I tend to disagree. Imagine you have refactored some widely used function. This can be a change of many lines of code, for which compiling a subsection of is impossible. So after days of work, you press 'Build' for the first time and after 20 minutes you get a cryptic message about functions that differ only in their return type. Granted, most compilers give a decent trace such that the investigation into what happened would not take too long. However, an investigation shouldn't even be necessary in the first place.

The second point is definitely more concerning. Not only does ASSERT_SINGLE_INSTANTIATION not assert what it promises to assert, moreover the resulting program is ill-formed with no diagnostic required.

Stateful metaprogramming

Template metaprogramming is a technique in C++ to take advantage of the type system to generate code. In a few previous blog posts (e.g. 'Constrain your user-defined conversions' and 'Compile-time sizes for range adaptors') we made use of this to move computation from runtime to compile time.

Up to a few weeks ago I lived in the blissful assumption that C++ template metaprogramming was purely functional programming. Oh boy, was I wrong. In a purely functional programming language all functions are side-effect-free. For templates, this is true, only most of the time. With some magical friend injection, we can keep track of, and change state. Surprisingly many posts are already written about stateful metaprogramming, e.g. 'Revisiting Stateful Metaprogramming in C++20' and 'How to Hack C++ with Templates and Friends'.

Our previous solution hinted to stateful metaprogramming. Let us now take full advantage of it to fix the cryptic error message. In our case, the state we need to keep track of is if something was instantiated before. The essence can be boiled down to the following code. Here, tc::string_template_param is a compile-time string.

template<tc::string_template_param strFile, int nLine>
struct InstantiationLocation final {
	friend auto InstantiatedFlag(InstantiationLocation) noexcept;           // (1)
};

template<typename Location>
struct SetInstantiation final {                                             // (2)
	friend auto InstantiatedFlag(Location) noexcept {}
};

template<typename Location, auto UniqueTag>
[[nodiscard]] consteval auto Instantiate() noexcept {                       // (3)
	if constexpr(requires { InstantiatedFlag(Location()); }) {
		return false;
	} else {
		SetInstantiation<Location>();
		return true;
	}
}

At (1) we declare (not define) a function with auto return type. As long as the compiler doesn't know about a definition this function cannot be called. Only when we would instantiate (2), will a definition be injected.

Now we can use our ability to call (1) as an indicator for whether we already instantiated something. This check happens in (3), if we already can call InstantiatedFlag we return false to indicate that the instantiation already happened. Otherwise, we inject a definition for (1).

This utility can be used with a single line:

static_assert(Instantiate<InstantiationLocation<__FILE__, __LINE__>, []{}>(), "Should only be instantiated once!");

What about multiple translation units

When using multiple translation units, the story is more complicated. As the compiler only works on a single translation unit, it is up to the linker to verify that instantiation only happened once. Unfortunately, we have found no reliable way to let the linker throw an error; the C++ standard contains "No diagnostic required" for all errors we could think of.

The only remaining way to ensure single instantiation is at runtime. Ideally the program should detect, and consequently potentially fail, as early as possible. Waiting until both instantiations are called is not robust and may incur a performance penalty. Using global constant initializer we can do all checks, even before main is entered.

template<typename Location>
void AssertSingleInstantiation() noexcept {
	static constinit bool bIsInstantiated = false;
	assert(!bIsInstantiated); // multiple instantiations across compilation units
	bIsInstantiated = true;
}

template<typename Location, auto UniqueTag>
inline const auto c_AssertSingleInstantiationBeforeMain = []() noexcept {
	AssertSingleInstantiation<Location>();
	return 0;
}();

Combining the static_assert from before and an instantiation of c_AssertSingleInstantiationBeforeMain yields the following macro:

#define _ASSERT_SINGLE_INSTANTIATION { \
	using Location = InstantiationLocation<__FILE__, __LINE__>; \
	static_cast<void>(c_AssertSingleInstantiationBeforeMain<Location, []{}>); \
	static_assert(Instantiate<Location, []{}>(), "Should only be instantiated once!"); \
}

You can experiment with it yourself in compiler explorer!

Checking a property at runtime that (at least in principle) should be checked during compilation is not great. However, running those checks before main is the next best thing. If you can find a way to reliably let the linker throw an error, please do let us know!

Bonus

In MSVC there is, at the time of writing, a code generation bug with static variables. Using a lambda (instead of the address of a static variable) as a unique tag avoids this bug.

— by Toon Baeyens

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