compile time typeid for every type
Asked Answered
A

6

13

I'd like a constexpr function that will return me a unique id for every C++ type, something like this:

using typeid_t = uintptr_t;

template <typename T>
constexpr typeid_t type_id() noexcept
{
  return typeid_t(type_id<T>);
}

int main()
{
  ::std::cout << ::std::integral_constant<typeid_t, type_id<float>()>{} << ::std::endl;

  return 0;
}

But I get an error:

t.cpp: In function 'int main()':
t.cpp:23:69: error: conversion from pointer type 'typeid_t (*)() noexcept {aka long unsigned int (*)() noexcept}' to arithmetic type 'typeid_t {aka long unsigned int}' in a constant-expression
   ::std::cout << ::std::integral_constant<typeid_t, type_id<float>()>{} << ::std::endl;
                                                                     ^
t.cpp:23:69: note: in template argument for type 'long unsigned int' 

Is there a workaround or another way?

Autogamy answered 20/3, 2016 at 18:38 Comment(7)
How's taking a pointer to the function here: (typeid_t(type_id<T>)) is supposed to work? Why don't you just use typeid operator?Anagrammatize
Because its not constexpr?Autogamy
Only if you apply it to the polymorphic typeAnagrammatize
Can you give a short example where typeid would work as a template parameterAutogamy
Note that typeid(expr).hash_code() is almost what you want, but it is not a constexpr.Depress
@Depress Thank you, but I did want a constexpr solutionAutogamy
I prefer using the "actual" type name, rather than an opaque numeric ID; see this question.Mcculley
O
9

You could use some tricks as shown in this answer.

There's even a library called ctti that utilizes the same trick, it should work out of the box

static_assert(ctti::type_id<int>() != ctti::type_id<float>(), "compile-time type-id comparison");

constexpr auto hash = ctti::type_id<int>().hash();
Opprobrious answered 21/3, 2016 at 2:12 Comment(0)
S
4

Another way, this time involving a constexpr function, would be to use a well known hash function like in the following example (where I used the FNV v1a):

#include <cstdint>
#include <iostream>

static constexpr uint32_t offset = 2166136261u;
static constexpr uint32_t prime = 16777619u;

constexpr uint32_t helper(uint32_t partial, const char *str) {
    return str[0] == 0 ? partial : helper((partial^str[0])*prime, str+1);
}

constexpr uint32_t hash_str(const char *input) {
    return helper(offset, input);
}

struct MyClassA { static constexpr uint32_t type = hash_str("MyClassA"); };
struct MyClassB { static constexpr uint32_t type = hash_str("MyClassB"); };

int main() {
    std::cout << "MyClassA: " << MyClassA::type << std::endl;
    std::cout << "MyClassB: " << MyClassB::type << std::endl;
}

The drawbacks:

  • conflicts can happen
  • error-prone (at least, from my point of view)
  • pretty invasive a sollution

The main advantage is that you can use this solution if you need the types to be the same over different executions (as an example, if you have to store them somewhere and use them again after a while).

Sextuplet answered 20/3, 2016 at 20:33 Comment(1)
That's why I consider it error-prone, it's pretty easy to forget adding your string time to time. Anyway, that solution offers the chance to have persistent types, so it depends of what's the goal you want to achieve.Sextuplet
M
3
template<auto Id>
struct counter {
    using tag = counter;

    struct generator {
        friend consteval auto is_defined(tag)
        { return true; }
    };
    friend consteval auto is_defined(tag);

    template<typename Tag = tag, auto = is_defined(Tag{})>
    static consteval auto exists(auto)
    { return true; }

    static consteval auto exists(...)
    { return generator(), false; }
};

template<typename T, auto Id = int{}>
consteval auto unique_id() {
    if constexpr (counter<Id>::exists(Id)) {
        return unique_id<T, Id + 1>();
    } else {
        return Id;
    }
}

static_assert(unique_id<int>() == 0);
static_assert(unique_id<int>() == 0);
static_assert(unique_id<double>() == 1);
static_assert(unique_id<float>() == 2);
static_assert(unique_id<char>() == 3);

int main(){
    return 0;
}

https://godbolt.org/z/GY74xvq5q

consteval since -std=c++20, based on: https://mcmap.net/q/20768/-does-c-support-compile-time-counters

It can be adapted to c++17 just changing consteval to constexpr. In the need to adapt to c++14 auto on templated must be changed, and if constexpr replaced.

It's guaranteed to generate the same ids for any compilation as long as the types always follow the same initialization order.

Marchese answered 12/9, 2023 at 23:0 Comment(2)
C++17 doesn't allow auto as an argument of a function. Instead of exists(auto) one can use decltype from the deduced type. Also, it is interesting how it works: I've seen implementations that rely on <auto = []{}> and only for C++20. This, however, indeed works with C++17 and also with other compilersUlland
godbolt.org/z/a54jG8hn3 here's a C++17 version with fixed warningsUlland
M
3
#include <cassert>
#include <cstdint>
#include <iostream>

template <class C>
struct TypeIdentifier {
    constexpr static int _id{};
    constexpr static auto id() {
        return &_id;
    }
};


int main() {
    constexpr auto const id_int = TypeIdentifier<int>::id();
    constexpr auto const id_int_again = TypeIdentifier<int>::id();
    constexpr auto const id_float = TypeIdentifier<float>::id();
    constexpr auto const id_double = TypeIdentifier<double>::id();
    constexpr auto const id_char = TypeIdentifier<char>::id();

    // Test equality
    static_assert(id_int == id_int_again);
    static_assert(id_int != id_float);

    std::cout << id_int << std::endl;
    std::cout << id_float << std::endl;
    std::cout << id_double << std::endl;
    std::cout << id_char << std::endl;
    
    assert(id_int == id_int_again);
    
    return 0;
}

https://godbolt.org/z/sETffooxT

There is this nice way to get a compile time type id. The values will mostly change between one compilation and another due to the use of addresses, but for the purpose of hashing/switching/jumps it's a safe bet.

Marchese answered 13/9, 2023 at 0:2 Comment(0)
S
1

This's not a constexpr function, but if there is not the constraint for the types to be persistent over multiple executions, you can use CRTP idiom as an alternative approach to achieve the same result.
It follows a minimal, working example:

#include <cstddef>
#include <iostream>

struct BaseClass {
protected:
    static std::size_t next() noexcept {
        static std::size_t counter = 0;
        return counter++;
    }
};

template<class D>
struct Base: public BaseClass {
    static std::size_t type() noexcept {
        static std::size_t type_ = BaseClass::next();
        return type_;
    }
};

struct A: public Base<A> { };
struct B: public Base<B> { };

int main() {
    std::cout << A::type() << std::endl;
    std::cout << B::type() << std::endl;
}

This way you have a base class from which to derive for all those types for which you would like to have an unique identifier.

Sextuplet answered 20/3, 2016 at 20:21 Comment(2)
I like this. A class template like that could support all types, though not at compile time.Autogamy
Another drawback is that a class can easily have different types during different executions, so this is a good solution only if you need consistency over a single execution of your software.Sextuplet
T
1

I just added https://github.com/CarloWood/ai-utils/blob/master/type_id_hash.h Test case: godbolt

It uses the same method as ctti, but way more compact ;).

Trough answered 3/5 at 15:16 Comment(4)
here's an example that will not work: godbolt.org/z/qrhTe65YjUlland
@SergeyKolesnik It works when using clang: godbolt.org/z/4sKnbKn3E gcc uses the same name for anonymous structs.Trough
this makes the solution not portable in this regard (perhaps, with lambdas also). This should be mentionedUlland
I never said it was portable (or support anonymous structs). I said it did the same thing as github.com/Manu343726/ctti - but more compact.Trough

© 2022 - 2024 — McMap. All rights reserved.