Skip to content

Instantly share code, notes, and snippets.

@JAJames
Created December 8, 2021 22:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JAJames/11ab5af1bf5beecfff72387188692933 to your computer and use it in GitHub Desktop.
Save JAJames/11ab5af1bf5beecfff72387188692933 to your computer and use it in GitHub Desktop.
C++17: Abusing side effects of static_assert around template types to implement "anonymous" static initialization

You cannot directly static-initialize a class without naming the instantiation:

struct some_class;

// ... some_class defined somewhere else ...

static some_class; // Invalid
static some_class{}; // Invalid
inline static some_class; // Invalid
inline static some_class{}; // Invalid

And you also cannot make a class instantiate itself:

struct some_class {
	inline static some_class s_instance{}; // Invalid; some_class is not a complete type
}

There are many other ways to static initialize a class though, often used as part of a self-registration pattern. You can concatenate the class name with _instance for example, or the line number, or specialize a separate template class, or whatever. These patterns work, and there aren't serious problems with them. Sure, you're relying on preprocessor string concatenation, or you're relying that nowhere else do you need to reuse the name. The biggest inconvenience is it polluting auto-completion / intellisense.

And you can't just make a static_initializer class to instantiate it, because then you still have to instantiate the template class and use it in some way somewhere else in your code. Which just puts you back at square one... or does it?

There's one way you can instantiate that template class without referencing it anywhere in runtime code: using it in a static_assert.

namespace static_initializer {

template<typename T>
struct instance_container {
	static constexpr T* instance() {
		return &s_instance;
	}

	inline static T s_instance{};
};

} // namespace static_initializer

// ... Some .cpp file
struct some_class {
	some_class() {
		std::cout << "test text" << std::endl;
	}
};

static_assert(static_initializer::instance_container<some_class>::instance() != nullptr);
// Neither some_class nor instance_container<some_class> are referenced anywhere else

Since the static_assert has to resolve instance_container<some_class>, it should instantiate all static members of that class. Even though the value for instance() is thrown away, and indeed instance() itself could very well not make it to the end binary, the static_assert is still asserting that instance() would return a valid pointer if called. The compiler cannot safely optimize out instance_container<some_class>::s_instance here. Since this isn't a static function variable, it will be initialized at static initialization rather than be delayed.

This works on GCC >= 8.4 | >= 9.3 and Clang >= 5.0 with -std=c++17, as well as MSVC >= 19.14 with /std:c++17. Macro helpers are provided via [static_initializer.hpp] and [static_initializer_multi.hpp].

Caveats:

  • Everything that comes with using an inline static variable.
  • The instance can still be referenced by code, albeit through static_initializer::instance(). If that's a deal-breaker for whatever reason, move s_instance to private and bit-or s_instance with some garbage. Then it'll be truly impossible to reference by name or instance_container. That does not mean it's truly impossible to reference.
  • The syntax still isn't as pretty as desired.
  • If a class needs to be static initialized more than once for whatever reason, you have to incorporate __FILE__ and __LINE__ magic this is supposed to avoid, or some other magic/workaround.
  • It's non-trivial to pass arguments to the constructor. Either template parameters must be used, or instance_container must be specialized. A macro could provide that specialization, but namespacing has to be kept in mind, and most of the point of this exercise is to give a feasible no-macro static initializer.

Key benefits:

  • It's a self-contained self-registration pattern with no chance for naming collisions, so long as a class only need be instantiated once (i.e: singletons).
  • No need for macros, for default-constructed singletons.
  • Code auto-completion isn't polluted

Other notes: What I was originally looking for was a way to abuse static_assert to test if arbitrary code compiles or not, without naming a function. That can be done as below:

#define ASSERT_COMPILES(expr) static_assert([]() { expr; } != nullptr, "Your compiler is whack yo");
#define ASSERT_COMPILES_CONSTEXPR(expr) static_assert([]() constexpr { expr; } != nullptr, "Your compiler is whack yo");
/** for classes which should only be instantiated once, i.e: self-registration patterns */
namespace static_initializer {
template<typename T>
struct instance_container {
static constexpr T* instance() {
return &s_instance;
}
inline static T s_instance{};
};
} // namespace static_initializer
#define STATIC_INITIALIZE_CLASS(IN_CLASS) \
static_assert(static_initializer::instance_container<IN_CLASS>::instance() != nullptr);
// The static_assert probably isn't even needed here, but hey might as well
#define STATIC_INITIALIZE_CLASS_WITH_ARGS(IN_CLASS, ...) \
template<> struct static_initializer::instance_container<IN_CLASS> { \
static constexpr IN_CLASS* instance() { return &s_instance; } \
inline static IN_CLASS s_instance{ __VA_ARGS__ }; }; \
static_assert(static_initializer::instance_container<IN_CLASS>::instance() != nullptr);
/** for classes which need to be instantiated multiple times, for whatever reason */
namespace static_initializer_multi {
// fnv1a, but could (should?) be anything else
constexpr uint64_t hash(const char* in_filename, size_t in_line) {
uint64_t result_hash = 14695981039346656037ULL;
while (*in_filename != '\0') {
result_hash ^= *in_filename;
result_hash *= 1099511628211ULL;
++in_filename;
}
result_hash ^= in_line;
return result_hash;
}
template<typename T, uint64_t in_identifier>
struct instance_container {
static constexpr T* instance() {
return &s_instance;
}
inline static T s_instance{};
}; // struct instance_container
} // namespace static_initializer
#define STATIC_INITIALIZE_CLASS_MULTI(IN_CLASS) \
static_assert(static_initializer_multi::instance_container \
<IN_CLASS, static_initializer_multi::hash(__FILE__, __LINE__)>::instance() != nullptr);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment