Unique compile time (constexpr) type ID without RTTI 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&lt;typename T&gt;
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&lt;typename T&gt;
const T* const type_id_ptr&lt;T&gt;::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 &lt;typename T&gt;
constexpr auto type_id() noexcept -&gt; type_id_t {
    return &amp;detail::type_id_ptr&lt;T&gt;::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&lt;int&gt;()};
constexpr auto id_of_float_type = type_id_t{type_id&lt;float&gt;()};

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&lt;type_id_t, void*&gt; anything;

// sorry for raw new
anything[type_id&lt;std::string&gt;()] = 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&lt;type_id_t, void*, 2&gt;{
    {type_id&lt;int&gt;(), &amp;static_int},
    {type_id&lt;double&gt;(), &amp;static_double}
};
I'm convinced that your design will work correctly. Uniqueness of static variables is guaranteed, and they won't be optimized away if you define and odr-use them (taking the address of a variable is odr-using it). Running it through stack-overflow would be a good idea though, because it will probably catch the eye of one of those 200K+ reputation guru who answer all the tricky language questions.

What I can't understand is what you're trying to achieve with this. The only useful property of this compile-time id is its uniqueness, so it doesn't achieve anything more than the type itself: what is the difference between if (type_id&lt;A&gt;() == type_id&lt;B&gt;()) and if (std::is_same_v&lt;A, B&gt;)? I would argue that it achieves a lot less, because types have other useful traits (for a partial but ready-made list, just take a look at the type_traits header in the standard library).

Besides, your id generator isn't truly generic: type_id&lt;int&amp;&gt;() won't compile because a pointer can't point to a reference (thus static const int&amp;* const id is illegal). Of course there's always the possibility to remove the reference, but it means that you'll use type_traits to make your type_id work -so it underlines that you should use well-established, standard type traits instead of this new compile-time type_id.

I'd suggest, if it is possible, that you post here a larger part of your code,
 containing use cases, and let our collective brain work on the solution of the broader problem.



Edit: I understand now better what you want to do. I really like the simplicity of use, and I find the hack very clever. Actually, if it was uglier, limitations like no-references-allowed wouldn't matter much, because everyone would see it as work-around. But since it seems so fluent, it could quickly become pervasive in the code; then limitations matter, all the more when they result in obscure error messages.

Anyway, I believe I would have come up with a less ambitious design, which would also be easier to maintain and understand (this concrete implementation relies on C++17 but it wouldn't be to difficult to implement in previous standards):

#include &lt;iostream&gt;
#include &lt;vector&gt;

template &lt;typename... Types&gt;
struct Type_register{};

template &lt;typename Queried_type&gt;
constexpr int type_id(Type_register&lt;&gt;) { static_assert(false, "You shan't query a type you didn't register first"); return -1; }

template &lt;typename Queried_type, typename Type, typename... Types&gt;
constexpr int type_id(Type_register&lt;Type, Types...&gt;) {
    if constexpr (std::is_same_v&lt;Type, Queried_type&gt;) return 0;
    else return 1 + type_id&lt;Queried_type&gt;(Type_register&lt;Types...&gt;());
}

int main() {
   Type_register&lt;int, float, char, std::vector&lt;int&gt;, int&amp;&gt; registered_types;
   constexpr auto test1 = type_id&lt;int&amp;&gt;(registered_types);
   constexpr auto test2 = type_id&lt;int*&gt;(registered_types);
}