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\$
3
  • \$\begingroup\$ the type id trick won't work across shared libraries. \$\endgroup\$ May 29, 2014 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, 2014 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\$ Mar 28, 2019 at 8:33

2 Answers 2

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
  • 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, 2014 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\$
8
  • \$\begingroup\$ Doesn't work across the shared libraries. The one with the function template instantiation address does work however. \$\endgroup\$
    – Velkan
    Jan 18, 2017 at 9:04
  • \$\begingroup\$ could be you did not export the function. Anyway, I've provided an alternative now. \$\endgroup\$ Jan 18, 2017 at 9:28
  • \$\begingroup\$ Doesn't work with VS :( it always return same address. \$\endgroup\$
    – Gelldur
    Jan 20, 2019 at 19:25
  • \$\begingroup\$ @DawidDrozd if you instantiate it with the same T, this is what it should do. \$\endgroup\$ Jan 20, 2019 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\$ Jan 21, 2019 at 15:50

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.