An Embedded Engineer’s Blog

とある組み込みエンジニアの備忘録的なブログです。

C++で任意のデータ型をシリアライズ - その1 インタフェース定義編

まえがき

C++で任意のデータをシリアライズするためのクラスの実装方法を紹介します。

プログラムで扱っているデータをネットワーク経由で送受信する場合や、ファイルに保存する場合にはシリアライズ(直列化)してバイナリ配列や文字列などに変換する必要があります。

バイナリ形式に変換するパターンとテキスト形式(XML/JSON)に変換するパターンの両方を紹介したいと思います。

今回はインタフェースの定義の実装について説明します。

ソース一式はGitHubで公開しています。


前提条件

今回実装したソースは以下の環境でビルド、動作確認しています。

OS Ver Compiler Remarks
Windows 11 Visual Studio 2022(C++14)


実装

インタフェース定義

まずは、ユーザに公開するインタフェースの定義を行います。 公開するインタフェースは主に以下の3つです。

  1. アーカイブ
  2. リアライザ
  3. ファクトリ


1. アーカイブ

アーカイブは、シリアライズされたデータ(バイナリ配列)およびそのサイズを保持するクラスです。
シリアライズ/デシリアライズ時のデータの書き込み/読み出し用のインタフェースも定義します。


アーカイブクラス(ヘッダ)
/* アーカイブ(シリアライズデータ)クラス */
class Archive
{
public:
    /* コンストラクタ */
    Archive();

    /* コンストラクタ(メモリ確保サイズ指定) */
    Archive(size_t size);

    /* デストラクタ */
    ~Archive();

    /* データバッファ取得(ユニークポインタ参照) */
    const std::unique_ptr<byte_t[]>& GetData() const;

    /* データバッファ取得(ポインタ参照) */
    const byte_ptr_t GetDataPtr() const;

    /* データバッファサイズ取得 */
    size_t GetSize() const;

    /* データバッファメモリ確保 */
    void Reserve(size_t size);

    /* 状態リセット(データバッファメモリ開放) */
    void Reset();

    /* 指定オフセット位置に1byteデータ書き込み */
    void Write(const byte_t& in_byte, size_t& offset);

    /* 指定オフセット位置から1byteデータ読み込み */
    void Read(byte_t& out_byte, size_t& offset) const;

    /* 指定オフセット位置から文字列データ書き込み */
    void Write(const string_t& in_str, size_t& offset);

    /* 指定オフセット位置から終端(null文字まで)文字列データ読み込み */
    void Read(string_t& out_str, size_t& offset) const;

    /* 指定オフセット位置から指定サイズ分文字列データ読み込み */
    void Read(string_t& out_str, size_t& offset, size_t length) const;

    /* 指定オフセット位置からバイト配列書き込み */
    template<size_t N>
    void Write(const std::array<byte_t, N>& in_bytes, size_t& offset)
    {
        for (size_t i = 0; i < N; i++)
        {
            this->m_Buffer[offset + i] = in_bytes[i];
        }
        offset += N;
    }

    /* 指定オフセット位置からバイト配列読み込み */
    template<size_t N>
    void Read(std::array<byte_t, N>& out_bytes, size_t& offset) const
    {
        for (size_t i = 0; i < N; i++)
        {
            out_bytes[i] = this->m_Buffer[offset + i];
        }
        offset += N;
    }

private:
    /* シリアライズデータ書き込みバッファ */
    std::unique_ptr<byte_t[]> m_Buffer;

    /* データバッファサイズ */
    size_t m_Size;
};


アーカイブクラス(ソース)
/* コンストラクタ */
Archive::Archive()
    : m_Buffer()
    , m_Size(0)
{
    /* Nothing to do */
}

/* コンストラクタ(メモリ確保サイズ指定) */
Archive::Archive(size_t size)
    : m_Buffer()
    , m_Size(0)
{
    /* データバッファメモリ確保 */
    this->Reserve(size);
}

/* デストラクタ */
Archive::~Archive()
{
    /* 状態リセット(データバッファメモリ開放) */
    this->Reset();
}

/* データバッファ取得(ユニークポインタ参照) */
const std::unique_ptr<byte_t[]>& Archive::GetData() const
{
    return this->m_Buffer;
}

/* データバッファ取得(ポインタ参照) */
const byte_ptr_t Archive::GetDataPtr() const
{
    return this->m_Buffer.get();
}

/* データバッファサイズ取得 */
size_t Archive::GetSize() const
{
    return this->m_Size;
}

/* データバッファメモリ確保 */
void Archive::Reserve(size_t size)
{
    /* メモリ確保済みの場合は状態リセット */
    if (this->m_Buffer != nullptr)
    {
        this->Reset();
    }

    /* データバッファサイズセット */
    this->m_Size = size;
    /* データバッファメモリ確保 */
    this->m_Buffer = std::make_unique<byte_t[]>(size);
}

/* 状態リセット(データバッファメモリ開放) */
void Archive::Reset()
{
    /* データバッファリセット(メモリ開放) */
    this->m_Buffer.reset();
    this->m_Buffer = nullptr;

    /* データバッファサイズクリア */
    this->m_Size = 0;
}

/* 指定オフセット位置に1byteデータ書き込み */
void Archive::Write(const byte_t& in_byte, size_t& offset)
{
    /* 範囲外チェック */
    if (offset >= this->m_Size)
    {
        THROW_FATAL_EXCEPTION(STRING_FORMAT("Offset is out of range : offset=%d size=%d", offset, this->m_Size));
    }

    /* 指定オフセット位置にデータセット */
    this->m_Buffer[offset] = in_byte;

    /* オフセットをインクリメント */
    offset += 1;
}

/* 指定オフセット位置から1byteデータ読み込み */
void Archive::Read(byte_t& out_byte, size_t& offset) const
{
    /* 範囲外チェック */
    if (offset >= this->m_Size)
    {
        THROW_FATAL_EXCEPTION(STRING_FORMAT("Offset is out of range : offset=%d size=%d", offset, this->m_Size));
    }

    /* 指定オフセット位置のデータ読み込み */
    out_byte = this->m_Buffer[offset];

    /* オフセットをインクリメント */
    offset += 1;
}

/* 指定オフセット位置から文字列データ書き込み */
void Archive::Write(const string_t& in_str, size_t& offset)
{
    /* 範囲外チェック */
    if (offset >= this->m_Size)
    {
        THROW_FATAL_EXCEPTION(STRING_FORMAT("Offset is out of range : offset=%d size=%d", offset, this->m_Size));
    }

    /* 文字列の長さ取得 */
    size_t text_len = in_str.length();

    /* 範囲外チェック(オフセットから文字列の終端 + null) */
    if ((offset + text_len + 1) > this->m_Size)
    {
        THROW_FATAL_EXCEPTION(STRING_FORMAT("Offset + Length is out of range : offset=%d text_len=%d size=%d", offset, text_len, this->m_Size));
    }

    /* 指定オフセット位置から文字列をコピー */
    memcpy(this->m_Buffer.get() + offset, in_str.c_str(), text_len);

    /* 文字列長分オフセット */
    offset += text_len;

    /* 終端にnull文字セット */
    this->m_Buffer[offset] = '\0';

    /* 終端(null文字)サイズ分オフセット */
    offset += 1;
}

/* 指定オフセット位置から終端(null文字まで)文字列データ読み込み */
void Archive::Read(string_t& out_str, size_t& offset) const
{
    /* 範囲外チェック */
    if (offset >= this->m_Size)
    {
        THROW_FATAL_EXCEPTION(STRING_FORMAT("Offset is out of range : offset=%d size=%d", offset, this->m_Size));
    }

    /* 指定オフセット位置から終端までを文字列として取得 */
    out_str = std::string(reinterpret_cast<const char*>(this->m_Buffer.get() + offset));

    /* 文字列の長さ取得 */
    size_t text_len = out_str.length();

    /* 文字列長文 + 終端(null文字)サイズ分オフセット */
    offset += (text_len + 1);
}

/* 指定オフセット位置から指定サイズ分文字列データ読み込み */
void Archive::Read(string_t& out_str, size_t& offset, size_t length) const
{
    /* 範囲外チェック */
    if (offset >= this->m_Size)
    {
        THROW_FATAL_EXCEPTION(STRING_FORMAT("Offset is out of range : offset=%d size=%d", offset, this->m_Size));
    }

    /* 範囲外チェック */
    if (offset + length > this->m_Size)
    {
        THROW_FATAL_EXCEPTION(STRING_FORMAT("Offset + Length is out of range : offset=%d length=%d size=%d", offset, length, this->m_Size));
    }

    /* 末尾がnull文字 */
    if (*reinterpret_cast<const char*>(this->m_Buffer.get() + offset + length - 1) == '\0')
    {
        /* 指定オフセット位置から終端までを文字列として取得 */
        out_str = std::string(reinterpret_cast<const char*>(this->m_Buffer.get() + offset), length - 1);

        /* 文字列の長さ取得 */
        size_t text_len = out_str.length();

        /* データサイズチェック */
        if ((text_len + 1) != length)
        {
            THROW_FATAL_EXCEPTION(STRING_FORMAT("Data Size unmatch : text_len=%d length=%d", (text_len + 1), length));
        }
    }
    else
    {
        /* 指定オフセット位置から終端までを文字列として取得 */
        out_str = std::string(reinterpret_cast<const char*>(this->m_Buffer.get() + offset), length);

        /* 文字列の長さ取得 */
        size_t text_len = out_str.length();

        /* データサイズチェック */
        if (text_len != length)
        {
            THROW_FATAL_EXCEPTION(STRING_FORMAT("Data Size unmatch : text_len=%d length=%d", text_len, length));
        }
    }

    /* データサイズサイズ分オフセット */
    offset += length;
}


2. シリアライザ

リアライザは実際にデータをシリアライズ/デシリアライズするためのインタフェースを提供する抽象クラスです。
任意のデータ型に対応できるようにするためテンプレートクラスとして定義します。

リアライザクラス
/* シリアライザクラス */
template <typename T>
class Serializer
{
public:
    /* テンプレートで指定されたデータ型をシリアライズしてアーカイブに変換 */
    virtual void Serialize(const T& in_data, Archive& out_archive) = 0;

    /* アーカイブをデシリアライズしてテンプレートで指定されたデータ型に変換 */
    virtual void Deserialize(const Archive& in_archive, T& out_data) = 0;
};


Serializerクラスを継承してバイナリ形式用のシリアライザとテキスト形式用のシリアライザクラスを定義します。

バイナリ形式シリアライザクラス
/* バイナリ形式シリアライザクラス */
template <typename T>
class BinarySerializer : public Serializer<T>
{
public:
    /* テンプレートで指定されたデータ型をシリアライズしてアーカイブに変換 */
    void Serialize(const T& in_data, Archive& out_archive) override
    {
        /* バイナリ形式シリアライズクラスシングルトンインスタンス取得 */
        auto& binary_serialization = BinarySerialization::GetInstance();

        /* 入力データをシリアライズしてアーカイブに変換 */
        binary_serialization.Serialize(in_data, out_archive);
    }

    /* アーカイブをデシリアライズしてテンプレートで指定されたデータ型に変換 */
    void Deserialize(const Archive& in_archive, T& out_data) override
    {
        /* バイナリ形式シリアライズクラスシングルトンインスタンス取得 */
        auto& binary_serialization = BinarySerialization::GetInstance();

        /* アーカイブをデシリアライズして出力データに変換 */
        binary_serialization.Deserialize(in_archive, out_data);
    }
};


テキスト形式シリアライザクラス
/* テキスト形式シリアライザクラス */
template <typename T>
class TextSerializer : public Serializer<T>
{
public:
    /* テンプレートで指定されたデータ型をシリアライズしてアーカイブに変換 */
    void Serialize(const T& in_data, Archive& out_archive) override
    {
        /* テキスト形式シリアライズクラスシングルトンインスタンス取得 */
        auto& text_serialization = TextSerialization::GetInstance();

        /* ルートテキストセット */
        string_t name = "root";

        /* 入力データをシリアライズしてアーカイブに変換 */
        text_serialization.Serialize(in_data, name, out_archive);
    }

    /* アーカイブをデシリアライズしてテンプレートで指定されたデータ型に変換 */
    void Deserialize(const Archive& in_archive, T& out_data) override
    {
        /* テキスト形式シリアライズクラスシングルトンインスタンス取得 */
        auto& text_serialization = TextSerialization::GetInstance();

        /* ルートテキストセット */
        string_t name = "root";

        /* アーカイブをデシリアライズして出力データに変換 */
        text_serialization.Deserialize(in_archive, name, out_data);
    }
};


実際のシリアライズ/デシリアライズ処理は、次回以降に紹介するBinarySerializationクラスおよびTextSerializationクラスで行います。


3. ファクトリ

ファクトリは任意の型に対するシリアライザを生成するファクトリクラスです。
バイナリ形式用のシリアライザと、テキスト形式用のシリアライザを生成するためのインタフェースを提供します。


リアライザファクトリクラス
/* シリアライザファクトリクラス */
template <typename T>
class SerializerFactory
{
public:
    /* バイナリ形式のシリアライザ生成 */
    static Serializer<T>& CreateBinarySerializer()
    {
        static BinarySerializer<T> serializer;
        return serializer;
    }

    /* テキスト形式のシリアライザ生成 */
    static Serializer<T>& CreateTextSerializer()
    {
        static TextSerializer<T> serializer;
        return serializer;
    }
};

CreateBinarySerializer()CreateTextSerializer()でそれぞれ、テンプレートパラメータで指定されたデータ型に対応したBinarySerializerクラスおよびTextSerializerクラスのインスタンスを静的に生成し、そのインスタンスの参照を返します。

あとがき

今回はシリアライズ処理のインタフェース定義を実装しました。
次回は、バイナリ形式でのシリアライズ処理の実装について説明します。