6

I've seen many posts explaining how to generate a unique id for a class.

In my case, the id is chosen by the user (for various reasons), but I want to make sure that no id is used twice in different classes.

I reduced my problem to the following code :

struct A {}; struct B {};

template <typename T> struct traits {};
template <> struct traits<A> { static constexpr size_t id() { return 0; }}
template <> struct traits<B> { static constexpr size_t id() { return 1; }}

Now, is there an easy way for me to make sure that someone does not add a specialization of the trait with a duplicated id :

struct C {};
template <> struct traits<C> { static constexpr size_t id() { return 1; // this should static_assert ! }}

I can use C++11, and I don't want to abuse the pre-processor.

If possible, the solution should not require anything special from the code specializing the trait (i.e if the check can be done externally by looking at already existing specializations, it would be great).

Thanks

  • 1
    I normally solve problems like this simply by declaring all the id's in one place. Then it's obvious if there are duplicates. – Paul Sanders Oct 04 '18 at 10:57
  • 1
    But the specialization and ids are added one by one by other people, in different headers ... I cannot do this – user1766172 Oct 04 '18 at 11:01
  • 1
    As a programming standard, could you not have a common header file containing `constexpr int idA = 0; constexpr int idB = 1;...` and then use `idA, idB...` in the template definitions themselves? I see no better way to achieve what you want to achieve, not at compile time anyway. – Paul Sanders Oct 04 '18 at 11:13
  • I can't have a common header, the types A, B .. are added in different libraries that share the traits definition... – user1766172 Oct 04 '18 at 11:16
  • 2
    Sure you can: just `#include ids.h` in every header that defines such a template. – Paul Sanders Oct 04 '18 at 11:17
  • No, I mean ids.h (if it existed) would be read-only for other projects, it would have to know about all the ids in subprojects. There can't be a single header with all known and future ids. – user1766172 Oct 04 '18 at 11:25
  • 2
    Can it not be under version control? – Paul Sanders Oct 04 '18 at 11:27

2 Answers2

4

Here is one idea, which I am not sure how applicable it may be for you:

#include <cstddef>

struct A {}; struct B {}; struct C {};

template <size_t Id> constexpr size_t getId() { return Id; }

template <typename T> struct traits {};

template size_t getId<0>();
template <> struct traits<A> { static constexpr size_t id() { return getId<0>(); }};

template size_t getId<1>();
template <> struct traits<B> { static constexpr size_t id() { return getId<1>(); }};

/* This fails to compile
template size_t getId<1>();
template <> struct traits<C> { static constexpr size_t id() { return getId<1>(); }};
*/

int main() { return 0; }

This relies on using explicit template instantiation of a function. The pitfall, obviously, is that you may forget to add this instantiation and still use the function, and then it would not fail to compile. You could define some preprocessor macro to ensure traits are always defined with it.


EDIT: As pointed out by Oliv, the above solution only works when all template instantiations happen in the same translation unit. This version works across compilation units, although it is more prone to mistakes (template parameters and return values must match).

#include <cstddef>

struct A {}; struct B {}; struct C {};

template <size_t Id> constexpr size_t getId();

template <typename T> struct traits {};

template <> constexpr size_t getId<0>() { return 0; }
template <> struct traits<A> { static constexpr size_t id() { return getId<0>(); }};

template <> constexpr size_t getId<1>() { return 1; }
template <> struct traits<B> { static constexpr size_t id() { return getId<1>(); }};

/* This fails to compile
template <> constexpr size_t getId<1>() { return 1; }
template <> struct traits<C> { static constexpr size_t id() { return getId<1>(); }};
*/

int main() { return 0; }

EDIT 2: I posted the question Why does explicit template instantiation not break ODR? due to a behavior I found surprising while writing this answer. See there for more detail on what may or may not fail to compile.

jdehesa
  • 58,456
  • 7
  • 77
  • 121
  • I like the idea ! I really was hoping for a solution that avoid having to use the pre-processor, but if I don't find another solution, I'll accept it – user1766172 Oct 04 '18 at 11:16
  • 1
    But this will compile if this template instantiation definition happens in an other translation unit. – Oliv Oct 05 '18 at 08:21
  • @Oliv You are right, thanks for pointing that out. I have added a modification that (I think) should work fine for multiple translation units. However, I thought that explicitly instantiating a template with the same parameters in different compilation units would break ODR and cause a linking error... do you know why this is not the case? (or I can post a question about it I guess) – jdehesa Oct 05 '18 at 09:33
  • 1
    @jdehesa I thought the same, but I checked the standard, the [relevant paragraph](http://eel.is/c++draft/temp.explicit#13)... I cannot conclude anything with this. So I tested with gcc, it does not complain. The instantiated template definition are declared as weak symbols. – Oliv Oct 05 '18 at 10:18
  • @Oliv Thank you for the reference and for testing that out. I have posted [another question](https://stackoverflow.com/q/52664184) to get a more complete clarification. – jdehesa Oct 05 '18 at 11:04
1

It sounds bad and it's not perfect, but you may force quite good uniqueness by calculating some hash function from concatenated __LINE__ and __FILE__ macro defines:

constexpr std::uint32_t calculate_hash(std::uint32_t seed, const char data[])
{
    //some hashing algorithm
}

#define CALCULATE_HASH_FROM_LINE() calculate_hash(__LINE__, __FILE__) 

struct A {};
struct B {};
struct C {};

template <typename T> struct traits {};

template <> struct traits<A> { static constexpr size_t id() { return CALCULATE_HASH_FROM_LINE(); }}; 
template <> struct traits<B> { static constexpr size_t id() { return CALCULATE_HASH_FROM_LINE(); }};
template <> struct traits<C> { static constexpr size_t id() { return CALCULATE_HASH_FROM_LINE(); }};

Full code: http://coliru.stacked-crooked.com/a/c1805baf9863b238

Pros:

  • standard-conforming
  • easy to use, potentially hard to break

Cons:

  • defining two things in one line will fail
  • doesn't work at all with templates

Other way is to use macro to extract type name:

constexpr std::uint32_t calculate_hash(std::uint32_t seed, const char data[])
{
    //some hashing algorithm
}

#define CALCULATE_HASH(T) calculate_hash(29, #T) 

struct A {};
struct B {};
struct C {};

template <typename T> struct traits {};

template <> struct traits<A> { static constexpr size_t id() { return CALCULATE_HASH(A); }}; 
template <> struct traits<B> { static constexpr size_t id() { return CALCULATE_HASH(B); }};
template <> struct traits<C> { static constexpr size_t id() { return CALCULATE_HASH(C); }};

Full code: http://coliru.stacked-crooked.com/a/46b00f9aea039c5b This method have the same pros and cons as previous, but additionally:

Pros:

  • more related to the actual type than previous method

Cons:

  • more information to pass to macro
  • specializations of "traits" will have to contain full namespace to wanted type (which also means that all specializations should be probably placed in one namespace). Failing to do that can end up with name collisions.
  • ::A will generate different hash than A

So, both ways will probably work in many cases, but are essentially wrong. C++ needs compile-time reflection badly : /.

Michał Łoś
  • 633
  • 4
  • 15