18
\$\begingroup\$

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}
};
\$\endgroup\$
12
  • 1
    \$\begingroup\$ This relies on a kernel that does address space layout randomization, along with plenty of other assumptions about how the system allocates memory. Have you done randomness tests to see if this entropy pool is reliable on your system (leaving alone other people's systems)? There are other, possibly better, ways to have compile-time randomness. Are you trying to do something like library order randomization (ie since OpenBSD 6), I'm not sure I see the purpose? \$\endgroup\$
    – esote
    Dec 19, 2018 at 0:49
  • 3
    \$\begingroup\$ @esote My goal is not to generate compile time randomness, but instead to have a unique value for a given type. I use the ID as key in a std::map and a compile time map. \$\endgroup\$ Dec 19, 2018 at 0:53
  • 2
    \$\begingroup\$ This really requires a language-lawyer tag. Great question, but unfortunately beyond me. \$\endgroup\$
    – vnp
    Dec 19, 2018 at 2:07
  • 4
    \$\begingroup\$ 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 where typeid doesn't work, but for type_id does work. \$\endgroup\$ Dec 19, 2018 at 7:39
  • 1
    \$\begingroup\$ @CrisLuengo I added some motivation / example of uses. Tell me if this is enough. \$\endgroup\$ Dec 19, 2018 at 15:39

2 Answers 2

12
\$\begingroup\$

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<A>() == type_id<B>()) and if (std::is_same_v<A, B>)? 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<int&>() won't compile because a pointer can't point to a reference (thus static const int&* 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 <iostream>
#include <vector>

template <typename... Types>
struct Type_register{};

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

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

int main() {
   Type_register<int, float, char, std::vector<int>, int&> registered_types;
   constexpr auto test1 = type_id<int&>(registered_types);
   constexpr auto test2 = type_id<int*>(registered_types);
}
\$\endgroup\$
3
  • 1
    \$\begingroup\$ You are correct, references won''t work, but I think I'll simply require that T is an object type for now. Thanks! Also, for the usefulness of the code, it allow me using a frozen::unordered_map with type_id_t as key. RTTI is also quite costly and many disable it, so we can safely replaces most previous use of RTTI without paying heavy cost for classes that don't use it. \$\endgroup\$ Dec 19, 2018 at 13:46
  • \$\begingroup\$ I added some examples of uses. \$\endgroup\$ Dec 19, 2018 at 15:42
  • \$\begingroup\$ @GuillaumeRacicot: thanks, I've edited my answer to take the examples into account \$\endgroup\$
    – papagaga
    Dec 19, 2018 at 16:43
0
\$\begingroup\$

I really liked your design, but for a reason that I'm not sure to understand it actually yields the same value for any type (your static_assert breaks).

Perhaps it's due to my compiler, Visual Studio 2019, breaking the technique because of its identical COMDAT folding linker optimization, like in this answer from another similar question.

Basically the compiler falsely assumes that it can throw away template information and do the same logic for any type, so the same value will be returned each time.

But for instance, this approach works on my machine. I like yours better though! :-(

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Welcome to Code Review. Can you please explain why you like the code? Your compiler doesn't matter in an answer unless you are asserting there is something wrong with the code. \$\endgroup\$
    – pacmaninbw
    Sep 13, 2020 at 12:01
  • \$\begingroup\$ "The code doesn't work as intended in Visual Studio." That seems like a very useful review comment to me. :-\ \$\endgroup\$
    – BTownTKD
    Oct 6, 2021 at 15:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.