When I started writing this series, the approach used to make EnTT work across boundaries was questionable. It worked but in a cumbersome manner that was difficult to work with, sometimes unclear or even dangerously close to being unstable.

I tried to give some insights of this approach with my first post. Long story short, that was a fully runtime solution used to generate sequential identifiers that worked just fine for a self-contained application. However, one could get in troubles across boundaries because of them.

Nowadays, EnTT relies on a completely different approach. It mixes standard and non-standard features in a single solution that is also sfinae-friendly and therefore fully customizable in case users already have their own type id generation system.

The current generator is constexpr in the best case but it has also a runtime fallback if needed.

If you haven’t done it yet, read the previous parts of the series before to continue. They will probably help to fully understand this one, even though they aren’t strictly necessary.

Introduction

In C++, there isn’t a reliable way to associate unique identifiers to our types. One could argue the there exists an std::type_info class capable of returning information for a given type but:

These information are implementation defined.

There is no guarantee that they are stable across different runs.

It’s recommended to avoid collisions but it may still happen and there is no way to get around this in case.

Not exactly the right tool for our purposes most of the times. So, all in all, we cannot really differentiate among types in a safe manner out of the box.

A quick search online gives us many appealing solutions though. Some are based on a cast of a function pointer returned by the specialization of a function template, some others exploit a slightly different feature of the language to turn this in a sequential identifier.

Unfortunately, none of them is reliable, stable across different runs and usable across boundaries at the same time.

So am I telling you that there is no solution? Not quite.

We can approach it from multiple sides and try to get the best out of what we have.

Don’t panic

The following class is a refined version of my first attempt:

class generator { inline static std :: size_t counter {}; public: template < typename Type > inline static const std :: size_t type = counter ++ ; };

This works like a charm in a standalone application and can generate sequential identifiers that aren’t necessarily stable across different runs, nor across boundaries.

We can use it like this:

const auto type = generator :: type < my_class > ;

The basic idea is quite simple. We have a shared counter that is incremented every time we generate an identifier for a new type. Since type is a variable template, it forces an increment of counter with every specialization.

In case type<T> has been already initialized, its value is returned without affecting counter anymore.

This works because of how inline variables work. A thin layer of template machinery makes the magic happen.

However, these identifiers are generated at runtime and aren’t therefore that stable unless you force them to be so somehow.

Consider the following snippet:

if ( condition ) { const auto it = generator :: type < int > ; const auto ct = generator :: type < char > ; // ... } else { const auto ct = generator :: type < char > ; const auto it = generator :: type < int > ; // ... }

In one case int is assigned the identifier N and char is assigned the identifier N+1. In the other case, the two types have opposite identifiers instead. What will happen strictly depends on condition the first time we encounter this statement.

This is a trivial example that is unlikely to happen but in a complex codebase something like this could be lying everywhere, deeply buried under a chain of function calls or whatever.

Another problem of this solution is that it doesn’t work across boundaries in all cases.

On linux, with default visibility, it just works because everything is literally public and thus exposed to the linker. On Windows, it can easily break and we can have different identifiers assigned to the same type from different sides of a boundary.

With a bunch of dllimport , dllexport and __attribute__((visibility("default"))) we can get around it though but there are still problems with plugins:

struct GENERATOR_API generator { static std :: size_t next () { static std :: size_t value {}; return value ++ ; } }; template < typename Type > struct GENERATOR_API type { static std :: size_t id () { static const std :: size_t value = generator :: next (); return value ; } };

The slightly different form is due to some idiosyncrasies of MSVC that has some serious limitations when it comes to working across boundaries.

On the other hand, GENERATOR_API is the well known macro we find everywhere for this kind of things:

#if defined _WIN32 || defined __CYGWIN__ || defined _MSC_VER # define GENERATOR_EXPORT __declspec(dllexport) # define GENERATOR_IMPORT __declspec(dllimport) #elif defined __GNUC__ && __GNUC__ >= 4 # define GENERATOR_EXPORT __attribute__((visibility("default"))) # define GENERATOR_IMPORT __attribute__((visibility("default"))) #else /* Unsupported compiler */ # define GENERATOR_EXPORT # define GENERATOR_IMPORT #endif #ifndef GENERATOR_API # if defined GENERATOR_API_EXPORT # define GENERATOR_API GENERATOR_EXPORT # elif defined GENERATOR_API_IMPORT # define GENERATOR_API GENERATOR_IMPORT # else /* No API */ # define GENERATOR_API # endif #endif

This is a reduced version for the sake of brevity but I’m sure you know what I’m talking about at this point.

May the standard C++ be with you

The standard is our source of truth. It’s also very clear when it comes to dispelling doubts about what it means to work across boundaries.

We can sum up all it spends on this topic with the following:

…

Absolutely nothing. Working across boundaries isn’t something that the standard takes or should even take in consideration and, in fact, it doesn’t help much here.

This means that there isn’t a standard way to do that. This cannot mean anything good though.

However, the standard may perhaps help us with our goal. Most likely there is something that can help us define stable type identifiers that work well also across boundaries.

We played with function templates so far. Let’s see if we can get more out of them.

Also the standard disappoints

Apparently, __func__ seems to be a good candidate for extrapolating some useful information from a function. At least that was what I thought the first time I saw it. Well, it is not.

The uselessness of __func__ in C++ cannot be easily described in my opinion.

First of all, what’s that? It’s a predefined variable that is defined within the function body of every function. So far, so good. Sounds interesting.

The actual definition already raises some doubts though:

static const char __func__ [] = "function-name" ;

In other terms, this variable contains foo within the following function:

void foo ();

What if foo is a function template instead? It doesn’t matter. __func__ still contains foo and the template parameter used to specialize it is lost. It’s not part of this variable, that’s all.

Standardish alternatives

Unfortunately, there isn’t a refined counterpart of __func__ that works also with function templates in the standard. However, the major compilers introduced some standardish solutions to get around this limitation.

In particular, MSVC offers the __FUNCSIG__ macro while both clang and GCC make available the __PRETTY_FUNCTION_ identifier. With the most recent versions of these compilers, these literals are also constant expressions and therefore they can be used at compile-time.

Suppose now we have a compile-time hashing function (yes, it’s possible). With these things at hand, we can easily compute an identifier for a type during compilation and use it everywhere with no costs at runtime:

template < typename Type > struct generator { static constexpr std :: size_t id () { constexpr auto value = hash_fn ( GENERATOR_PRETTY_FUNCTION ); return value ; } };

Here GENERATOR_PRETTY_FUNCTION is an opaque identifier used to abstract the actual name on the different systems. Something along this line is enough in most of the cases:

#if defined _MSC_VER # define GENERATOR_PRETTY_FUNCTION __FUNCSIG__ #elif defined __clang__ || (defined __GNUC__) # define GENERATOR_PRETTY_FUNCTION __PRETTY_FUNCTION__ #endif

This solution isn’t perfect and doesn’t work with less recent compilers but it’s enough for our purposes. For the sake of brevity, I won’t spend more time on discussing how to refine it.

What makes it attractive instead is the fact that even though these identifiers are no longer sequential, they are at least stable across boundaries (well, under certain circumnstances but let’s assume this is always the case for a moment) and between different runs.

A refined solution

Let’s put everything together finally. This is what we obtain:

struct GENERATOR_API generator { static std :: size_t next () { static std :: size_t value {}; return value ++ ; } }; template < typename Type > struct GENERATOR_API type { #if defined GENERATOR_PRETTY_FUNCTION static constexpr std :: size_t id () { constexpr auto value = hash_fn ( GENERATOR_PRETTY_FUNCTION ); return value ; } #else static std :: size_t id () { static const std :: size_t value = generator :: next (); return value ; } #endif };

So far, so good. We have a generator that works at compile-time by relying on some non-standard solutions. Morever, there exists a runtime, fully standard fallback just in case.

We can even get around conflicts by means of a class specialization. In fact, since we are hashing strings on which we have no control, we cannot just ignore the problem this time.

The only thing we can’t do (yet) is to provide a generation function for a family of types, for example on a per-traits basis. This is where SFINAE can enter the game to add that bit of salt that makes our solution suitable for all purposes.

Welcome SFINAE

It’s not that hard to get the job done actually:

template < typename Type , typename = void > struct GENERATOR_API type { // ... };

By adding a second template parameter, we make it possible to specialize this class on a per-traits basis.

As an example, in case we want to use a built-in generation system for a bunch of types we receive from a third-party library:

template < typename Type > struct GENERATOR_API type < Type , std :: void_t < decltype ( Type :: id ()) >> { static constexpr std :: size_t id () { return Type :: id (); } };

Of course, I admit that it may not be the most attractive of the features and that we can live quite well without it but while I was there, why not…

Conclusion

The solution proposed in EnTT is slightly more complex than the one described but nothing particularly striking.

What left me stunned is that on the one hand there is no reliable way to uniquely identify a type in a stable manner across boundaries and between different runs, on the other there is no tool that can be used for the purpose.

Unfortunately, <typeinfo> doesn’t have much to offer in this sense, at least as I see it. It seems more like an attempt made without trying too hard for some reasons unknown to me.

To be honest, I don’t think the solution described above is the only one or the best to get around the lack of an appropriate tool but it gets the job done in a straightforward manner and this is what matters most of the times.

As usual, for any comment or suggestion, ping me. Especially this time!

This is the best that I’ve managed to do so far but I wouldn’t be disappointed to discover that there are alternative solutions that I have not yet considered.

Let me know that it helped

If you liked this post and want to say thanks, consider to star the GitHub project that hosts this blog. It’s the only way you have to let me know that you appreciate my work.

Thanks.