C++でUUID(GUID)風のデータを生成する

C++でUUID(GUID)風のデータ列を取得するを紹介したいと思います。

あくまで「風」なので実際に使用した結果、何らかの障害が発生したとしても責任は負えません。それでも良ければ以下参照ください。

確認環境

紹介する実装は以下環境で作成・確認しています。

  • VisualStudio2017(15.9.11)
  • VC++(C++11以上)
  • Windows10(1809)

実装したときのモチベーションは以下のおとり。

  • Windows.h は include したくない
  • uuid.h が見つからない?

UUIDとGUIDの違い

UUIDとは128bitの長さからなるデータの集合です。

このデータ長の大半を乱数などで生成することで、多数の環境で生成してもほぼ重複することが無いユニークなデータや文字列とすることができる識別子の規則を表します。

また、よく聞かれるのですが、「UUID」と「GUID」の違いは以下の通りです。

  • 「UUID」の方言(一部)が「GUID」
  • 「GUID」はWindowsで使用されているUUIDの方言
    • 以前Windowsで生成するGUIDは"バリアント"と呼ばれる部分のビット上位が"0b110"だった
    • しかし、今は上位2ビットが"0x10"で初期のUUIDと同じになっているのでUUIDとGUIDの仕様は同じ
      • RCF4122形式で統一されている
    • C#のGUIDクラスで生成される数列はUUIDと同じ。

使用方法

まずは使用方法です。

一種類の唯一、ひとつだけの使用方法しかありません。

UuidクラスのnewUuidメソッドを呼び出すと戻り値としてUUIDオブジェクトが取得できます。

生成したデータは、toByteもしくはtoStringで扱いやすいデータとして取得できます。

また、そのままUuidオブジェクト同士の比較もできます。(データ列からUuidオブジェクトに逆生成できないと微妙な気もしますが実装が面倒だったので実装してません。

#include <iostream>
#include <sstream>
#include <random>

#include "Guid.hpp"

int main()
{
    auto uuid = Uuid::newUuid(); // (1) UUIDの新規生成
    if (uuid == uuid)
    {
        // 比較演算子をオーバーロードしているのでコンテナ同士を比較できる
    }
    if (uuid != uuid)
    {
        // こっちも同じ
    }
    
    // (2) UUIDをバイト列として取り出す
    array<byte, 16> bytes = uuid.toByte();
    
    // (3) UUIDを文字列として取り出す
    string uuid_str = uuid.toString();

    // (4) 大量に生成しても重複しない
    for (int i = 0; i < 100; i++)
    {
        std::cout << Uuid::newUuid().toString(); << std::endl;
        // > 79b51811-3423-4229-838a-73c319e8e5fc
        // > ca65d996-f6e5-4d7c-9660-7dbf57e6bf29
        // > d5519cba-5901-46c0-b30b-81a960de6ebe
        // > 876186f6-1ed5-44af-a865-7d0e181ef682
        // ...
    }

    return 0;
}

ヘッダー(Guid.hpp)

Guid.hppの内容は以下の通り。

#pragma once

#include <sstream>
#include <random>
#include <stdint.h>
#include <array>

/**
 * UUID(GUID)を表すクラス
 */
class Uuid
{
public:

    Uuid() = default;
    ~Uuid() = default;
    Uuid(const Uuid&) = default;
    Uuid& operator=(const Uuid&) = default;
    Uuid(Uuid&&) = default;
    Uuid& operator=(Uuid&&) = default;

    // 新しいUUIDを生成する
    static Uuid newUuid();
    // データを文字列として取り出す。"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"形式
    std::string toString();
    // データを16バイトのストリームとして取り出す
    std::array<unsigned char, 16> toByte();

    // 比較演算子
    bool operator==(const Uuid& a);
    bool operator!=(const Uuid& a);

private:

    // データ管理用のコンテナ
    union _TempU64
    {
        uint64_t      a;
        uint32_t      b[2];
        uint16_t      c[4];
        unsigned char d[8];
    };

    // 一意正性を確保するためインスタンスは全部共有
    static bool isInit;
    static std::mt19937 mt;
    static std::uniform_int_distribution<uint64_t> range;

    // C#のGUIDと同じデータ構造を取る
    uint32_t a;
    uint16_t b;
    uint16_t c;
    unsigned char d[8];

    // UIntSplitを乱数で埋めて返す
    static _TempU64 gen();

    // 16進変換 + 上位不足桁をゼロ埋め
    template<typename CNT>
    static std::string toHex(CNT value);
    static std::string toHex(unsigned char value[]);
};

template<typename CNT>
inline std::string Uuid::toHex(CNT value)
{
    int size = sizeof(value);
    
    std::stringstream ss;
    ss << std::hex << (int)value;
    std::string str = ss.str();

    std::string temp = "";
    if (str.length() < size)
    {
        for (int i = 0; i < size - str.length(); i++)
        {
            temp.append("0");
        }
    }

    return temp + str;
}

実装(Guid.cpp)

Guid.cppの内容は以下の通り。

すいません、めちゃくちゃ長くなってしまいました…(汗

もう少し(かなり?)効率よく書けると思いますが、そこらへんは各自でカスタマイズしてください。

#include "Uuid.hpp"

bool Uuid::isInit = false;
std::mt19937 Uuid::mt;
std::uniform_int_distribution<uint64_t> Uuid::range;

Uuid Uuid::newUuid()
{
    if (!isInit) // エンジン初期化
    {
        isInit = true;
        std::random_device rnd;
        mt = std::mt19937(rnd());
        range = std::uniform_int_distribution<uint64_t>(0x1000000000000000, 0xFFFFFFFFFFFFFFFF);
    }

    _TempU64 upper = gen();
    _TempU64 lower = gen();
    
    Uuid ret;
    ret.a = upper.b[0];
    ret.b = upper.c[2];
    ret.c = upper.c[3];
    for (int i = 0; i < 8; i++) ret.d[i] = lower.d[i];

    // ver.4 sign
    ret.c = ((ret.c & 0x0FFF) + 0x4000);
    // RFC4122 sign
    ret.d[0] = ((ret.d[0] & 0x3F) + 0x80);

    return ret;
}

std::string Uuid::toString()
{
    return toHex(this->a).append("-").append(toHex(this->b))
        .append("-").append(toHex(this->c)).append("-").append(toHex(this->d));
}

std::array<unsigned char, 16> Uuid::toByte()
{
    _TempU64 upper;
    upper.b[0] = this->a;
    upper.c[2] = this->b;
    upper.c[3] = this->c;

    auto array = std::array<unsigned char, 16>();
    for (int i = 0; i < 8; i++) array[i] = upper.d[i];
    for (int i = 8; i < 16; i++) array[i] = this->d[i - 8];

    return array;
}

bool Uuid::operator==(const Uuid& a)
{
    return
        this->a == a.a &&
        this->b == a.b && this->c == a.c &&
        this->d[0] == a.d[0] && this->d[1] == a.d[1] && this->d[2] == a.d[2] && this->d[3] == a.d[3] &&
        this->d[4] == a.d[4] && this->d[5] == a.d[5] && this->d[6] == a.d[6] && this->d[7] == a.d[7];
}

bool Uuid::operator!=(const Uuid& a)
{
    return
        this->a != a.a ||
        this->b != a.b || this->c != a.c ||
        this->d[0] != a.d[0] || this->d[1] != a.d[1] || this->d[2] != a.d[2] || this->d[3] != a.d[3] ||
        this->d[4] != a.d[4] || this->d[5] != a.d[5] || this->d[6] != a.d[6] || this->d[7] != a.d[7];
}

Uuid::_TempU64 Uuid::gen()
{
    _TempU64 value;
    value.a = range(mt);
    return value;
}

std::string Uuid::toHex(unsigned char value[])
{
    std::string str;
    int len = sizeof(value) / sizeof(unsigned char);
    for (int i = 0; i < len; i++)
    {
        str.append(toHex(value[i]));
    }
    return str;
}

実装の説明

ほぼ説明不要かと思いまが、内容は全桁を乱数で生成しした後に、バージョン4の書式を明示するために1桁上書き + RCF4122準拠を示すためにバリアントに署名したものを文字列として返しています。文字列を生成するために4回も乱数を作成している + そのあと文字列にしているのでパフォーマンスはお察しでした。

簡単ですが以上です。