I am making a library where I need to generate a unique ID for a type where the ID must be known at compile time. I first relied on the address of a template function, but it proved itself unreliable both with MSVC and Clang on windows.
Then I came up with the address of a static data member inside a template class.
I want to support most use cases as possible here's what comes to my mind:
- Eager optimizations that would merge the definition of static variables or inline functions
- DLL and shared libraries
- Link time optimizations
My tests were successful, but it wasn't an exotic setup with multiple level dlopen
or stuff like that. I'm not an expert with the kind of use case I want to support so it's hard to tell if I can ensure my claims are okay.
My design work by declaring a static data member of type T*
inside a template class, then taking its address as the value of the ID.
namespace detail {
/**
* Template class that hold the declaration of the id.
*
* We use the pointer of this id as type id.
*/
template<typename T>
struct type_id_ptr {
// Having a static data member will ensure (I hope) that it has only one address for the whole program.
// Furthermore, the static data member having different types will ensure (I hope) it won't get optimized.
static const T* const id;
};
/**
* Definition of the id.
*/
template<typename T>
const T* const type_id_ptr<T>::id = nullptr;
} // namespace detail
/**
* The type of a type id.
*/
using type_id_t = const void*;
/**
* The function that returns the type id.
*
* It uses the pointer to the static data member of a class template to achieve this.
* Altough the value is not predictible, it's stable (I hope).
*/
template <typename T>
constexpr auto type_id() noexcept -> type_id_t {
return &detail::type_id_ptr<T>::id;
}
It there any caveats to be aware with this design?
Here's some usage of this and motivation for this code:
constexpr auto id_of_int_type = type_id_t{type_id<int>()};
constexpr auto id_of_float_type = type_id_t{type_id<float>()};
static_assert(id_of_int_type != id_of_float_type);
This can be compiled with -fno-rtti
.
Also, it can fully be used at compile time. Here's the equivalent with RTTI:
constexpr auto test = typeid(int); // won't compile
This code won't compile since typeid
cannot be used at compile time. Also, disabling RTTI also disables static RTTI.
The code is useful because it can be used in data structure or to be couple with compile time type erasure.
std::map<type_id_t, void*> anything;
// sorry for raw new
anything[type_id<std::string>()] = new std::string{"hello"};
// compile time example
static auto static_int = int{};
static auto static_double = double{};
// type_id as the keys in a compile time map
constexpr auto anything_compiletime = frozen::unordered_map<type_id_t, void*, 2>{
{type_id<int>(), &static_int},
{type_id<double>(), &static_double}
};
std::map
and a compile time map. \$\endgroup\$typeid
is resolved at compile time, if you don't apply it to a polymorphic type. With a polymorphic type, the type cannot be known at compile time, and thus your solution won't work either. I'm not sure what you're adding here. It would be nice if you could add an example usage wheretypeid
doesn't work, but fortype_id
does work. \$\endgroup\$