I had a similar problem a few months ago. I was looking for a technique to define identifiers that are the same over each execution.
If this is a requirement, here is another question that explores more or less the same issue (of course, it comes along with its nice answer).
Anyway I didn't use the proposed solution. It follows a description of what I did that time.
You can define a constexpr
function like the following one:
static constexpr uint32_t offset = 2166136261u;
static constexpr uint32_t prime = 16777619u;
constexpr uint32_t fnv(uint32_t partial, const char *str) {
return str[0] == 0 ? partial : fnv((partial^str[0])*prime, str+1);
}
inline uint32_t fnv(const char *str) {
return fnv(offset, str);
}
Then a class like this from which to inherit:
template<typename T>
struct B {
static const uint32_t id() {
static uint32_t val = fnv(T::identifier);
return val;
}
};
CRTP idiom does the rest.
As an example, you can define a derived class as it follows:
struct C: B<C> {
static const char * identifier;
};
const char * C::identifier = "ID(C)";
As long as you provide different identifiers for different classes, you will have unique numeric values that can be used to distinguish between the types.
Identifiers are not required to be part of the derived classes. As an example, you can provide them by means of a trait:
template<typename> struct trait;
template<> struct trait { static const char * identifier; };
// so on with all the identifiers
template<typename T>
struct B {
static const uint32_t id() {
static uint32_t val = fnv(trait<T>::identifier);
return val;
}
};
Advantages:
- Easy to implement.
- No dependencies.
- Numeric values are the same during each execution.
- Classes can share the same numeric identifier if needed.
Disadvantages:
- Error-prone: copy-and-paste can quickly become your worst enemy.
It follows a minimal, working example of what has been described above.
I adapted the code so as to be able to use the ID
member method in a switch
statement:
#include<type_traits>
#include<cstdint>
#include<cstddef>
static constexpr uint32_t offset = 2166136261u;
static constexpr uint32_t prime = 16777619u;
template<std::size_t I, std::size_t N>
constexpr
std::enable_if_t<(I == N), uint32_t>
fnv(uint32_t partial, const char (&)[N]) {
return partial;
}
template<std::size_t I, std::size_t N>
constexpr
std::enable_if_t<(I < N), uint32_t>
fnv(uint32_t partial, const char (&str)[N]) {
return fnv<I+1>((partial^str[I])*prime, str);
}
template<std::size_t N>
constexpr inline uint32_t fnv(const char (&str)[N]) {
return fnv<0>(offset, str);
}
template<typename T>
struct A {
static constexpr uint32_t ID() {
return fnv(T::identifier);
}
};
struct C: A<C> {
static constexpr char identifier[] = "foo";
};
struct D: A<D> {
static constexpr char identifier[] = "bar";
};
int main() {
constexpr auto val = C::ID();
switch(val) {
case C::ID():
break;
case D::ID():
break;
default:
break;
}
}
Please, note that if you want to use ID
in a non-constant expression, you must define somewhere the identifier
s as it follows:
constexpr char C::identifier[];
constexpr char D::identifier[];
Once you did it, you can do something like this:
int main() {
constexpr auto val = C::ID();
// Now, it is well-formed
auto ident = C::ID();
// ...
}
__FILE__
and__LINE__
. – KeyholeT
. – Twistint
, or can it be a different type (such asintptr_t
)? – GanttT
, doesn't have to change each compilation. At the time this was written people weren't understanding what a 'compile time constant' was, so it was easiest to use the switch example (cases in a switch need to be compile time constant). The result needs to be a compile time constant. – Troopship