20
\$\begingroup\$

Following my answer to Unique type ID in C++, I have worked towards a "safer" version that I am posting here.

This is a lightweight type that can store a unique (also across compilation units) ID per type for use at runtime, without RTTI.

Instead of using a built-in type for ID representation that is also accessible to the end user, a specific class type_id_t provides the external representation of the ID and encapsulates the actual storage type (a function pointer) that is not accessible.

A helper function template call type_id<T>() returns a type_id_t containing the ID of type T.

Hence, the following operations are only provided:

  • construction only by calling type_id function.
  • copy/move construction, as implicitly defined
  • copy/move assignment, as implicitly defined
  • comparison operators == and != between two type_id_t's

There is no default constructor for type_id_t.

Here is the implementation:

class type_id_t
{
    using sig = type_id_t();

    sig* id;
    type_id_t(sig* id) : id{id} {}

public:
    template<typename T>
    friend type_id_t type_id();

    bool operator==(type_id_t o) const { return id == o.id; }
    bool operator!=(type_id_t o) const { return id != o.id; }
};

template<typename T>
type_id_t type_id() { return &type_id<T>; }

It is interesting that function type_id returns a (function) pointer to itself, encapsulated into a type_id_t. No nasty type casting is necessary.

This is not a mechanism that provides runtime identification of a (polymorphic) object's type, but can be used as a low-level tool to build such functionality. I actually made this for the needs of my recent any, and then improved it.

Just for demonstration, we can store the ID of a number of types e.g. in a vector:

template<typename... T>
std::vector<type_id_t>
make_ids() { return {type_id<T>()...}; }

Later, we can compare the stored IDs to the ID of a given type:

template<typename T, typename A>
void comp_ids(const A& a)
{
    for (auto i : a)
        std::cout << (type_id<T>() == i) << " ";
    std::cout << std::endl;
}

and get the following:

auto ids = make_ids<
    int, bool, double const&, int&&, char(&)[8],
    int, void(int), unsigned char**, std::vector<void(*)()>, double[4][8]
>();

comp_ids<int>(ids);  // output: 1 0 0 0 0 1 0 0 0 0

Here is a live example.

Any comments are welcome.

\$\endgroup\$
  • \$\begingroup\$ the type id trick won't work across shared libraries. \$\endgroup\$ – user1095108 May 29 '14 at 8:46
  • \$\begingroup\$ @user1095108 Are you sure? I have posted a specific question on this. There has been some discussion and it appears that it will work. Bottomline is that templates are defined as weak symbols and are merged into one definition at link time according to iso 3.2/5. \$\endgroup\$ – iavr May 29 '14 at 9:00
  • \$\begingroup\$ Hi, is there any way, how to access type of type_id<T> later in the class? For debug purposes, would be great to be able to access some stringable object id (not pointer address). In VisualStudio, I see that sig has type t1 = {id=0x01bfdc52 {type_id<struct `....stTest>(void)} }. But I'm not able to get this value through typeid() in any way. Thanks. \$\endgroup\$ – Ludek Vodicka Mar 28 at 8:33
7
\$\begingroup\$

The first thing I notice when looking at this code is the lack of documentation.

I would provide some more operators. Specifically I would go for operator< so type_id_t is usable as key for std::map and you should think about specializing std::hash so the class can be used with hash maps.

As the code is pretty short I don't find any more to say about it.

Btw.: Well done. I like this solution very much.

\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks, very good point about operator< and std::hash! I thought == and != should be enough for identification, but being able to use ID as key in an associative container sounds very essential. As for documentation, this is my weakest point--I only hope this page could be seen as some form of documentation :-) \$\endgroup\$ – iavr Apr 30 '14 at 18:07
4
\$\begingroup\$

This is very nice, however, I usually just use:

using typeid_t = void const*;

template <typename T>
typeid_t type_id() noexcept
{
  static char const type_id;

  return &type_id;
}

The void const* pointers can be compared and hashed and cannot be dereferenced without casting. Good enough for me. The solution also avoids casts, just like your question.

Alternatively:

using typeid_t = void(*)();

template <typename T>
typeid_t type_id() noexcept
{
  return typeid_t(type_id<T>);
}
\$\endgroup\$
  • \$\begingroup\$ Doesn't work across the shared libraries. The one with the function template instantiation address does work however. \$\endgroup\$ – Velkan Jan 18 '17 at 9:04
  • \$\begingroup\$ could be you did not export the function. Anyway, I've provided an alternative now. \$\endgroup\$ – user1095108 Jan 18 '17 at 9:28
  • \$\begingroup\$ Doesn't work with VS :( it always return same address. \$\endgroup\$ – Dawid Drozd Jan 20 at 19:25
  • \$\begingroup\$ @DawidDrozd if you instantiate it with the same T, this is what it should do. \$\endgroup\$ – user1095108 Jan 20 at 20:48
  • 1
    \$\begingroup\$ @DawidDrozd put static in front of the function definition, there's always a way around vc++ strangeness. I always use this function as a class member not as a free function. \$\endgroup\$ – user1095108 Jan 21 at 15:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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