C++ Tricks: Type Counter Pattern
2024-09-06
There occasionally arises a need to count C++ types in a dynamic way instead of values.
Let me explain: lets say you're architecting a plugin system for your application, where users can extend a base Plugin
class. Your core implementation needs to store some metadata about each class inheriting from Plugin
registered to your plugin manager:
class Plugin { /* ... */ };
class PluginManager
{
// ...
template<typename T>
static void Register()
{
if constexpr(std::is_base_of<Plugin, T>())
{
// ...
// Registration and metadata logic
// ...
}
}
};
You would need some way to store the results of whatever operations you perform on the plugin. Directly indexing a type isn't possible in C++, and using RTTI through the typeid
operator (and std::type_index
) would preclude using an std::vector
to iterate through the registered types. How, then, do you proceed?
Below is a snippet of code representing an idea that's echoed through many of my personal projects that I affectinately call the type counter pattern:
class TypeCounter
{
private:
static size_t _count;
public:
template<typename T>
static constexpr size_t value()
{
static size_t val = _count++;
return val;
}
};
It is guaranteed to create a linear, and thus unique, sequence of numbers starting from 0 (or from whatever base value is deemed appropriate), making it perfect for generating unique identifiers for types, and unlike std::type_index
or std::type_info::hash_code
it allows these IDs to be used as lookups in non-associative containers, and even plain C-style arrays (gasp!). Using it is very simple:
std::cout << TypeCounter::value<MyPlugin>() << std::endl; // 0
std::cout << TypeCounter::value<MyOtherPlugin>() << std::endl; // 1
std::cout << TypeCounter::value<MyPlugin>() << std::endl; // 0, again!
Following the Plugin
example to its conclusion, TypeCounter
could be used to retrieve the index of an std::vector
containing the metadata of an already-registered Plugin
child class -- or, if the value returned by TypeCounter
is greater than or equal to the vector's .size()
, an assertion could be triggered or an exception thrown.
Another nicety about the type counter pattern is that, in addition to being legitimately useful, it also serves as an easy-to-digest stepping stone for introducing others to the idea of template metaprogramming. Compared to some of the more, uh, verbose black magic that can be invoked through templates, TypeCounter
tastefully illustrates the dance of C++ concepts at play that enable it: the static
keyword does most of the heavy lifting, first providing the _count
member variable that is not bound to a specific instance of TypeCounter
. Then, static
in the block-scope storage form causes val
to be initialized to the incremented value of _count
, which takes place the first time control passes through the declaration -- and because a templated function is instantiated once per unique value of its template parameters, a unique val
will be initialized for each T
and returned with each subsequent call to value()
.