5

I'd like to avoid assigning an Id to all my Classes where an Id is needed, so I wrote a small templated id generator.

BaseIdGenerator::makeUniqueId simply returns a new Id, everytime it is called:

class BaseIdGenerator {
protected:
    static inline Id makeUniqueId() {
        static Id nextId = 0;
        return nextId++;
    }
};

For assigning an Id to individual classes, the class is simply passed as a template argument to IdGenerator:

template <typename T>
Id getId();

template <typename T>
class IdGenerator final : public BaseIdGenerator {
    static Id id;

    template <typename Type>
    friend Id getId();
};

template <typename T>
inline Id getId() {
    return IdGenerator<T>::id;
}

template <typename T>
Id IdGenerator<T>::id = IdGenerator<T>::makeUniqueId();

This will call makeUniqueId() exactly one time per class (even at multithreaded applications since C++11 due to the thread safe local static variables)

In action this looks like this:

int main() {
    std::cout << getId<int>() << std::endl;  // prints 0
    std::cout << getId<bool>() << std::endl; // prints 1
    std::cout << getId<char>() << std::endl; // prints 2
}

This works as expected.

  • Is this well defined behavior in C++?
  • Will many uses of getId() break this functionality? (Multiple source files etc.)
  • Is this behavior standardized, so on every machine the output will be the same and in a network application it will work as expected?
Tim Diekmann
  • 7,755
  • 11
  • 41
  • 69
  • For the part about network, the question is not clear. If you have multiple applications or version of the same application you won't get the same Id each time for the same type. And if the code is not used as intended, then you can easily have different Ids. – Phil1970 Jun 17 '17 at 12:39
  • You might also use typeid / typeinfo if you only need a unique identifier per type. – Phil1970 Jun 17 '17 at 12:49
  • @Phil1970 I try to avoid RTTI because of very frequent lookup of the Id. With this approach it's possible to use the ids as keys in a continuous array. – Tim Diekmann Jun 17 '17 at 12:54
  • Well with RTTI you can use a `map` or an `hash_map` so it won't be that bad except if you overuse such Ids in your application which might be a sign of a poor design... In a properly designed C++ application, you should rarely need to check an object type. Usually if an operation depend of the type of an object, it should be a virtual function of that object hierarchy. A lot of `switch` statements in code is a code smell indicating that your code is not as object-oriented as it should be (polymorphism). – Phil1970 Jun 18 '17 at 12:15
  • @Phil1970 As I said, I try to avoid RTTI **because** I want to store the IDs in a continuous array, and a (unordered_)map doesn't fulfills this. Sure, a hash_map has a good random access time, but can't keep up with a vector. I don't need to check for the `Id` of an object, I just use this as a lookup for template-meta-programming for storing polymorphic classes in a single Vector. Due to this I have a fast access and an implicit check, that dynamic_cast won't fail or even better (if no virtual inheritance) I safely can replace dynamic_cast with static_cast. There are no `switch` in my code ;) – Tim Diekmann Jun 18 '17 at 12:45

2 Answers2

3

This will call makeUniqueId() exactly one time per class (even at multithreaded applications since C++11 due to the thread safe local static variables)

The initialization of the local static variables is thread safe. Not modifying them , so just having a static variable local in a function will make sure that it is constructed once and only once in multithreaded programs. Anything else that you do manually is prone to race conditions and requires synchronization on your end. For example, what you have above is prone to race conditions if you call makeUniqueId() concurrently from multiple threads.

This wikipedia article has a good explanation of how static local variable construction works and how they it is guarded from multithreaded access. But note again that it is only the construction of the static local variable that is protected by the language and the compiler.

Is this well defined behavior in C++?

As you have it right now, assuming all your code compiles, yes it is well defined behavior, it does not violate ODR as is if that is what you are thinking. However, if you specialize the getId() and/or IdGenerator class in an implementation file and link that to another file that does not see that specialization, then you are violating ODR, since now there are two definitions of the same thing in the system (one that is specialized and one that isn't). The compiler is not required to even warn with any diagnostics in that case. So although this works as is, be careful and know about ODR. For more see http://en.cppreference.com/w/cpp/language/definition

Will many uses of getId() break this functionality? (Multiple source files etc.)

You can use getId() as many times as you want. Static variable initialization order is not specified however, so getId() may not return the same values all the time. However you will have distinct values if you don't have races and ODR violations (as mentioned above)

Is this behavior standardized, so on every machine the output will be the same and in a network application it will work as expected?

Static variable initialization order is unspecified across translation units, so I would say it might not be the same on every machine. I didn't really follow how this would change for a network application. The code to initialize the values is ran before the program starts. So all the ids will be set before any network I/O (assuming you don't have network I/O in a static function called before main() is ran, in which case one of the values for getId() might not even be initialized)

Curious
  • 20,870
  • 8
  • 61
  • 146
  • "so getId() may not return the same values all the time". But they should always be the same value, if the first call is in the same order, right? – Tim Diekmann Jun 17 '17 at 12:35
  • 1
    @Tim it depends on how the compiler decides to order the initializations across the translation units. See https://stackoverflow.com/questions/211237/static-variables-initialisation-order for more details – Curious Jun 17 '17 at 12:36
  • @Tim And feel free to post a comment here if you have any more questions – Curious Jun 17 '17 at 12:37
  • 1
    alright, I won't rely on the order and will just use the Id on the local machine – Tim Diekmann Jun 17 '17 at 12:38
  • In Fact I use this generator to populate a map with the Id as a Key, e.g. in my [Event Dispatcher](https://codereview.stackexchange.com/questions/165822) (EventId = Id, getEventId() = getId()). `getId()` will only be called from the same source file, so there will be no problems with this approach I guess? – Tim Diekmann Jun 17 '17 at 12:46
  • 1
    @Tim if you are only calling this function from one source file then it will be the same every time on every machine. – Curious Jun 17 '17 at 12:47
2
  • Is this well defined behavior in C++?

Yes, it's well defined.

  • Will many uses of getId() break this functionality? (Multiple source files etc.)

No, not in general. But multithreaded access might cause race conditions.

  • Is this behavior standardized, so on every machine the output will be the same and in a network application it will work as expected?

Yes the behavior is standardized.

I don't see the interference with network application, besides endianess. Of course the resulting ID's will be in the same endianess as the host machine where they are generated. To avoid that use the htonl() function (assuming Id is a long integer type).

πάντα ῥεῖ
  • 1
  • 13
  • 116
  • 190