Skip to content

A modern, no-dependencies, portable C++ library for manipulating UUIDs. Fully supports RFC 9562 and RFC 4122.

License

Notifications You must be signed in to change notification settings

gershnik/modern-uuid

Repository files navigation

modern-uuid

Language Standard License Tests

A modern, no-dependencies, portable C++ library for manipulating UUIDs.

Features

  • Implements newer RFC 9562 (which supersedes older RFC 4122). Supports generation of UUID variants 1, 3, 5, 6 and 7.
  • Self-contained with no dependencies beyond C++ standard library.
  • Works on Mac, Linux, Windows, BSD, Wasm, and even Illumos. Might even work on some embedded systems given a suitable compiler and standard library support.
  • Requires C++20 but does not require a very recent compiler (GCC is supported from version 10 and clang from version 13).
  • Most operations (with an obvious exception of UUID generation and iostream I/O) are constexpr and can be done at compile time. Notably this enables:
    • Natural syntax for compile-time UUID literals
    • Using UUIDs as template parameters and in other compile-time contexts
  • Supports std::format (if available) for formatting and parsing in addition to iostreams.
  • Does not rely on C++ exceptions and can be used with C++ exceptions disabled.
  • Uses "safe" constructs only in public interface (no raw pointers and such).
  • Properly handles fork with no exec on Unix systems. UUIDs generated by the child process will not collide with parent's.

See also differences from other libraries below.

Usage

A quick intro to the library is given below. For more details see Usage Guide

#include <modern-uuid/uuid.h>

using namespace uuid;

//this is a compile time UUID literal
constexpr uuid u1("e53d37db-e4e0-484f-996f-3ab1d4701abc");

//default constructor creates Nil UUID 00000000-0000-0000-0000-000000000000
constexpr uuid nil_uuid;

//there is also uuid::max() to get Max UUID: FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF
constexpr uuid max_nil_uuid = uuid::max();

//if you want to you can use uuid as a template parameter
template<uuid U1> class some_class {...};
some_class<uuid("bc961bfb-b006-42f4-93ae-206f02658810")> some_object;

//you can generate all non-proprietary versions of UUID from RFC 9562:
uuid u_v1 = uuid::generate_time_based();
uuid u_v3 = uuid::generate_md5(uuid::namespaces::dns, "www.widgets.com");
uuid u_v4 = uuid::generate_random();
uuid u_v5 = uuid::generate_sha1(uuid::namespaces::dns, "www.widgets.com");
uuid u_v6 = uuid::generate_reordered_time_based();
uuid u_v7 = uuid::generate_unix_time_based();

//for non-literal strings you can parse uuids from strings using uuid::from_chars
//the argument to from_chars can be anything convertible to std::span<char, any extent>
//the call is constexpr
std::string some_uuid_str = "7D444840-9DC0-11D1-B245-5FFDCE74FAD2";
std::optional<uuid> maybe_uuid = uuid::from_chars(some_uuid_str);
if (maybe_uuid) {
    uuid parsed = *maybe_uuid;
}

//uuid objects can be compared in every possible way
assert(u_v1 > mil_uuid);
assert(u_v1 != u_v2);
std::strong_ordering res = (u_v6 <=> u_v7);
//etc.

//uuid objects can be hashed
std::unordered_map<uuid, transaction> transaction_map;

//they can be formatted. u and l stand for uppercase and lowercase

std::string str = std::format("{}", u1);
assert(str == "e53d37db-e4e0-484f-996f-3ab1d4701abc");

str = std::format("{:u}", u1);
assert(str == "E53D37DB-E4E0-484F-996F-3AB1D4701ABC")

str = std::format("{:l}", u1);
assert(str == "e53d37db-e4e0-484f-996f-3ab1d4701abc")

//uuids can be read/written from/to iostream 

//when reading case doesn't matter
std::istringstream istr("bc961bfb-b006-42f4-93ae-206f02658810");
uuid uuidr;
istr >> uuidr;
assert(uuidr = uuid("bc961bfb-b006-42f4-93ae-206f02658810"));

std::ostringstream ostr;
ostr << uuid("bc961bfb-b006-42f4-93ae-206f02658810");
assert(ostr.str() == "bc961bfb-b006-42f4-93ae-206f02658810");
ostr.str("");

//writing respects std::ios_base::uppercase stream flag
ostr << std::uppercase << uuid("7d444840-9dc0-11d1-b245-5ffdce74fad2");
assert(ostr.str() == "7D444840-9DC0-11D1-B245-5FFDCE74FAD2");

//uuid objects can be created from raw bytes
//you need an std::span<anything byte-like, 16> or anything convertible to 
//such a span
std::array<std::byte, 16> arr1 = {...};
uuid u_from_std_array(arr1);

uint8_t arr2[16] = {...};
uuid u_from_c_array(arr2);

std::vector<uint8_t> vec = {...};
uuid u_from_bytes(std::span{vec}.subspan<3, 19>());

//finally you can access raw uuid bytes via bytes public member
constexpr uuid ua("7d444840-9dc0-11d1-b245-5ffdce74fad2");
assert(ua.bytes[3] == 0x48);

//bytes is an std::array<uint8_t, 16> so you can use all std::array
//functionality
for(auto b: ua.bytes) {
    ...use the byte...
}

Building/Integrating

Quickest CMake method is given below. For more details and other method see Integration Guide

include(FetchContent)
FetchContent_Declare(modern-uuid
    GIT_REPOSITORY [email protected]:gershnik/modern-uuid.git
    GIT_TAG        <desired tag like v1.2 or a sha>
    GIT_SHALLOW    TRUE
)
FetchContent_MakeAvailable(modern-uuid)
...
target_link_libraries(mytarget
PRIVATE
  modern-uuid::modern-uuid
)

Differences from other libraries

There are two well-known libraries commonly used to handle UUIDs: libuuid from util-linux and Boost.Uuid. Both are very good libraries, but have, at the time of this writing (03-2025), limitations and/or trade-offs that I found inconvenient or annoying and which modern-uuid was created to address. In particular:

libuuid

Portability. libuuid is only really portable to Linux and BSD. It doesn't work on Windows. It hasn't even been working on Mac for almost a year now without patches. That's two major platforms out there.

Second, while its time-based generation algorithms are very robust and correct they assume that the system clock ticks once a microsecond. This assumption is not necessarily true. For example on WASM the clock ticks with a millisecond precision. This results in UUID generation being very slow on that platform and the results very predictable.

Lastly, libuuid is a C library. It doesn't really help C++ code to actually manipulate UUIDs as first class objects. For that one needs to either write a custom UUID class or use a different library.

Boost.Uuid

Boost.Uuid makes two design trade-offs that might not be the right ones for many users

It prioritizes speed above RFC compliance and, in some cases, correctness. For example it allows the sequential UUID counters to wrap around without waiting for the clock to change. This makes UUID v7 (and possibly v6 and v1 too) non always monotonically increasing on platforms with a slower clock (e.g. WASM again). The ways it populates v7 fields also contradicts RFC recommendations. Whether this results in some increased guessability or not is hard to tell. On the flip side this results in much faster UUID generation so YMMV.

It pushes management of UUID generator objects to the library user. Unfortunately, managing the generators is not a trivial task for most of them. You need to be aware of various intricacies that a casual user without deep understanding of how UUIDs work will likely miss. For example, would you be aware that you must reset any inherited parent process generator in a forked child process or risk duplicate UUIDs being generated?

This approach makes the library simpler and synthetic benchmarks faster. But it also makes it easy for the user to misuse the library and the speed gains would be negated by external generator management anyway.

Boost.Uuid is a header only library, which is great, but to actually use it you still need to get the entire 130+MB Boost-zilla download. If your project already contains Boost this is not a problem. But, for things that don't, it is a big annoyance.

modern-uuid tries to address these perceived shortcomings:

  • It strives to be widely portable to any reasonable system out there.
  • It does not require you to know how to manage generators. Just call generate_xxx and it will do the right thing.
  • It handles slow clocks correctly (hopefully)
  • It is standalone with no dependencies.

On the negative side:

  • It is slower than Boost.Uuid for UUID generation (but has the same performance as libuuid).
  • It is not header-only. (This might, or might not, be addressed in future releases)

Implementation details

There are many implementation choices for generating time-based UUIDs of versions 1, 6 and 7. This section documents some of them but these are not contractual and can change in future releases

For UUID version 6 the clock_seq and node field are populated using exactly the same data as for version 1.

For UUID version 7 the rand_a field is used to store additional clock precision using Method 3 of the section 6.2 of the RFC. This extends the precision of distinct representable times to 1µs. The first 14 bit of rand_b field are filled with a randomly seeded counter using Method 1 of the same section.

The actual granularity of the system clock is detected at runtime (unfortunately you cannot trust time_point::period on many systems). If the clock ticks slower than the maximum available precision for the desired UUID version then the unfilled rightmost decimal digits of the timestamp are filled by incrementing a counter for each generation. The counter is in the range [0, max-1) where max is the ratio of clock tick period to the desired precision (e.g. if the clock period is 1ms and desired precision is 1µs then max is 1,000). When the counter reaches max the generation waits until the clock changes for versions 1 and 6. For version 7 the rand_b field is used to provide further monotonicity as described above. When the rand_b counter is exhausted the generation finally waits for a clock change for version 7 too.

The clock_seq field for UUIDs version 1 and 6 as well as the equivalent first 14 bits of rand_b field of UUID version 7 are used to handle system clock going backwards and/or distinguish between UUIDs generated by different processes. It is used in the following manner:

  • On process startup (and upon fork() in child!) it is initialized to a random number
  • If the system clock goes backwards from the last generation it is incremented by 1 modulo 214

For time-based UUIDs (1, 6 and 7) in a multi-threaded environment there is a basic trade-off to be made between UUID monotonicity and speed/lack of contention. For version 1 monotonicity is not important and so modern-uuid by default allows each thread to generate them unsynchronized. Each thread gets a different clock_seq so the chance of collision is very low as long as you don't run huge number of threads. If you do, it is advisable to use version 6 or 7 instead. You can also override this behavior by providing custom clock persistence implementation.

For versions 6 and 7 monotonicity is generally important - this is after all one of the main reasons for their existence. Thus, modern-uuid by default synchronizes their generation between multiple threads. The values of UUIDs of each version are guaranteed to increase monotonically as long as the system clock doesn't go back. This of course increases thread contention. You can also override this behavior by providing custom clock_persistence implementation.

By default, if available, one of the system's network cards MAC addresses is used for UUIDs versions 1 and 6. If not available it is replaced by a random number (initialized once per process) as described in RFC 9562. You can change this behavior via set_node_id APIs. Alternatively, you can simply use UUID versions 7 or 4.