13
\$\begingroup\$

I need to have an unique ID for any type in C++ for a variant type. Is this code reliable for getting the ID? I don't care to have same ID for same type between multiple runs. Sorry for typos/formatting, I wrote the code on my phone and tested on Ideone.

#include <iostream>

struct Counter
{
    static size_t value;
};

size_t Counter::value = 1;

template<typename T>
struct TypeID : private Counter
{
    static size_t value()
    {
        static size_t value = Counter::value++;
        return value;
    }
};

int main()
{
    std::cout << TypeID<int>::value() << " " << TypeID<int*>::value() << " " << TypeID<Counter>::value() << " " << TypeID<int>::value();

return 0;
}

PS: I use this because std::type_index/std::type_info::hash may have the same value for different types (probably not in practice).

\$\endgroup\$
2
  • \$\begingroup\$ Use type_index. (Yours is a standard problem with a standard solution from the standard library, which is this one.) \$\endgroup\$
    – not-a-user
    Jun 23, 2019 at 13:59
  • \$\begingroup\$ @not-a-user it is bound to typeid, therefore can't be used with rtti disabled \$\endgroup\$ Jan 20, 2021 at 19:59

2 Answers 2

8
\$\begingroup\$

Re. the algorithm, IMO it looks safe, with only two caveats:

  • I'm not sure that it's thread-safe.

  • It looks safe, because of the "one-definition rule". I worried that if multiple source files ('translation units') invoke TypeID<int*>::value() they might each get their own copy of the type; however I think that the "one-definition rule" requires the linker to collapse the multiple types into one. Furthermore I think that's not just a common good-practice (implemented by some compilers but not others), but is actually required by the standard. But I don't think it will be safe if your program includes multiple DLLs: each DLL is built separately so it would get its own copy of a TypeID instantiation.

Re. the code, I don't see why TypeID is a subclass of Counter; it works equally well like this:

template<typename T>
struct TypeID // : private Counter
{
    static size_t value()
    {
        static size_t value = Counter::value++;
        return value;
    }
};

Perhaps you used inheritance to try to protect the Counter value; if so that would be more effective with the protected keyword:

struct Counter
{
protected:
    static size_t value;
};

That's still imperfect (because anyone can subclass Counter). So an all-in-one version would be more fool-proof:

class TypeID
{
    static size_t counter;

public:
    template<typename T>
    static size_t value()
    {
        static size_t id = counter++;
        return id;
    }
};

... invoked like this ...

TypeID::value<int>()

I think a spinlock + atomics will do the job. Do you think it is enough?

I found that for some (but perhaps not all) compilers and compiler options, initialization of statics is thread-safe: for detail, see Are (initializing) function static variables thread-safe in GCC?. In that case, using an atomic Counter::value is sufficient (because the initialization of each instance value is thread-safe).

If such a feature isn't in your compiler, then IMO you need:

  • a lockguard in the value() method
  • and the lockguard must lock an already-constructed lock (perhaps not a separate lock for each T)
  • but given a 'global' lockguard, it wouldn't be necessary to also use an atomic Counter::value
\$\endgroup\$
4
  • \$\begingroup\$ Thanks! Good catch with thread safety! I will use atomic increment for the value:) \$\endgroup\$ Mar 21, 2014 at 7:24
  • \$\begingroup\$ If it has to be thread-safe then atomic increment isn't enough. Initializing a static variable in a function is implicitly if (!initialized) id = etc. and that test for if (!initialized) isn't thread-safe. \$\endgroup\$
    – ChrisW
    Mar 21, 2014 at 7:45
  • \$\begingroup\$ I think a spinlock + atomics will do the job. Do you think it is enough? \$\endgroup\$ Mar 21, 2014 at 12:28
  • \$\begingroup\$ @Felics I edited my answer to try to reply to your comment. \$\endgroup\$
    – ChrisW
    Mar 21, 2014 at 14:07
17
\$\begingroup\$

Here is my lightweight solution that involves no operations at run time:

template<typename T>
struct type { static void id() { } };

template<typename T>
size_t type_id() { return reinterpret_cast<size_t>(&type<T>::id); }

struct A { };

int main ()
{
    cout << type_id<int>() << " " << type_id<int*>() << " "
         << type_id<A>() << " " << type_id<int>() << endl;
}

The type id of type T is nothing but the address of function type<T>::id, re-interpreted as a number. Being a static method, there is a unique such function per type. These addresses are assigned by the linker (I think), so they remain constant between different runs. The overhead in executable size is linear in the number of type ids requested (that is, the number of instantiations of type<T>).

This solution has been tested in gcc and clang, and works correctly across different compilation units, i.e. you get the same unique type id for the same type in different compilation units. However, I cannot quite explain why/how this happens.

type_id() is easily inlined, so has no run-time cost. However, since it contains reinterpret_cast (and since the function address is not known to the compiler), it cannot be made constexpr. But I don't see this as a problem: if you want to use something e.g. as a template argument, you can use the type directly instead of its id. The id is for run-time use only.

My original implementation is using const void* as the return type of type_id(), so I know it has the same size as any pointer. I adapted it to size_t to fit the question. I think this is still safe, but I can't see why const void* wouldn't be fair enough.

By the way, because I haven't used it very much, I'd be happy to hear if there may be problems with this approach.

\$\endgroup\$
8
  • \$\begingroup\$ However, I cannot quite explain why/how this happens. -- type<T>::id is a function (method). Apparently there's a so-called "one-definition rule" which requires the linker to ensure there's only one definition of each function. For example, what if the function had a local static variable: there should be only one. \$\endgroup\$
    – ChrisW
    Mar 22, 2014 at 12:40
  • \$\begingroup\$ Thanks. I only imagined that but I was never good enough at such issues. For instance, a local static variable needs (exactly) one out of class definition, which is not the case for a function, right? \$\endgroup\$
    – iavr
    Mar 22, 2014 at 13:12
  • \$\begingroup\$ Right. int foo() { static int count = 0; return count; } will return a count of the number of times the function has been called. There's only one instance of the function: it only has one address. If foo is a template, declared in a header and instantiated by/into several translation units, the linker needs to remove duplicate instances of it. \$\endgroup\$
    – ChrisW
    Mar 22, 2014 at 13:43
  • 4
    \$\begingroup\$ This solution will not work in VC++ with optimizations enabled due to COMDAT folding. In essence it's a linker optimization where all functions with an identical body will be replaced by a single function. Since type<T>::id() is identical for all T, it (and all other empty functions) will be replaced by a single function. This explains the behavior seen by @Julien. msdn.microsoft.com/en-us/library/bxwfs976.aspx \$\endgroup\$ Jun 22, 2018 at 12:28
  • 3
    \$\begingroup\$ This can be simplified to the following: template<typename T> struct type{static size_t id(){return reinterpret_cast<size_t>(&type<T>::id);}}; \$\endgroup\$
    – Kaiserludi
    Aug 22, 2018 at 18:21

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