Simple approach to unique identifiers

  • Post category:C++ / Snippets

Sometimes I need a class to have a unique identifier.
This identifier can than be used as a key to store and identify the class in an associative container (std::map / or std::unordered_map) or identify it uniquely across the application when logging etc.
In this post I like to present to you a quick and easy approach to realize this.

Widget with unique ID

For now we keep it simple and want a unique ID of type std::size_t. For more clarity we however use alias declarations so we can refer to it via Widget::ID. (Test with Godbolt Compiler Explorer).

/*
 * Widget.h
 */
#include <cstdint>
#ifndef WIDGET_H
#define WIDGET_H
class Widget
{
public:
    using ID = std::size_t;
    ID getID() const noexcept
    {
        return m_ID;
    }
private:
    const ID m_ID{createNewUniqueID()};
    ID createNewUniqueID() noexcept;
};
#endif
/*
 * Widget.cpp
 */
#include <atomic>
Widget::ID Widget::createNewUniqueID() noexcept
{
    static std::atomic<ID> s_identifier{0};
    return s_identifier++;
}

Why move something that simple to .cpp?

Though the function body is fairly simple we pin the implementation to our own translation unit.
This way we keep everything internal to Widget. Some thoughts:

  • The include for std::atomic does not propagate to client code when Widget.h is included
  • Its a private member function called once upon its construction. No need to “clobber” client code.

Why atomic?

We cannot control how or where our class will be created. We must take into account that Widget may be created in different threads, which increments s_identifier concurrently. As the increment is as well a read and a write to the same memory location we would get a data race. Lets look at the code in question:

static std::atomic<ID> s_identifier{0};
return s_identifier++;

We specified s_identifier to be of static storage duration. From section 6.7 of the C++ standard we can get that the initialization is perfectly fine when multiple callers reach this line:
When multiple concurrent callers get to this line, the first one takes care of the initialization; all others wait until finished (simplified).

The increment is a read and a write to the same memory location. Turning this into an atomic operation eliminates the data race. One way is to use std::atomic and its overloaded ++operator, that handles the atomic calls for us.

For a deep-dive on atomics, the C++ Memory Model, std::memory_order and sequential consistency I recommend the talks by Herb Sutter:

Herb Sutter – atomic Weapons: The C++ Memory Model and Modern Hardware