An Embedded Engineer’s Blog

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

C++で任意のデータ型をシリアライズ - その2 バイナリ形式シリアライズ編

まえがき

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

今回は前回に引き続きバイナリ形式でのシリアライズ処理の実装について説明します。

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


前提条件

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

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


実装

バイナリ形式シリアライズ

バイナリ形式シリアライズクラスでは、任意のデータ型をバイナリ形式(バイト配列)に変換して、アーカイブに書き込んでいく処理を行います。

ここでは、クラスの構成要素ごとに詳しく説明していきます。


メンバ変数

まず、メンバ変数として現在の実行環境のエンディアンと、シリアライズ後のデータのエンディアンを保持するようにしています。

/* 現在の環境のエンディアン */
endian::EndianType m_CurrentEndian;

/* シリアライズ後のエンディアン */
endian::EndianType m_TargetEndian;

一般的なPCなどで使用しているx86/x64系 CPUのエンディアンはリトルエンディアンですが、ネットワークで送受信するときのデータはビッグエンディアンを指定することが多く、また、マイコンなどの環境でもエンディアンが異なる可能性があるため、異なる環境間でも正しくシリアライズ/デシリアライズできるようにするためです。


コンストラク

シリアライズクラスは複数インスタンスを生成する必要がないため、シングルトンとして実装しています。
また、コンストラクタでは現在の環境のエンディアンおよびシリアライズ後のエンディアンを保持するメンバ変数を初期化しています。

public:
    /* シングルトンインスタンス取得 */
    static BinarySerialization& GetInstance()
    {
        static BinarySerialization instance;
        return instance;
    }

private:
    /* コンストラクタ */
    BinarySerialization()
        : m_CurrentEndian(endian::GetEnvironmentEndian())
        , m_TargetEndian(endian::EndianType::Network)
    {
        /* Nothing to do */
    }

    /* デストラクタ */
    ~BinarySerialization() = default;

public:
    /* コピー&ムーブセマンティクス無効化 */
    BinarySerialization(const BinarySerialization&) = delete;
    BinarySerialization(BinarySerialization&&) = delete;
    BinarySerialization& operator=(const BinarySerialization&) = delete;
    BinarySerialization& operator=(BinarySerialization&&) = delete;

現在の環境のエンディアンendian::GetEnvironmentEndian()で自動的に判定されるようにしており、シリアライズ後のエンディアンはネットワークで送受信するときに一般的に指定されるビッグエンディアンを指定しています。
endian::GetEnvironmentEndian()の詳細についてはGitHubのソースをご参照ください。


インタフェース

インタフェース関数として、シリアライズ後のデータサイズを算出するCalculateSize()、入力データをシリアライズしてアーカイブに変換するSerialize()アーカイブをデシリアライズして出力データに変換するDeserialize()を定義します。

public:
    /* シリアライズ後のデータサイズ算出 */
    template <typename T>
    void CalculateSize(const T& in_data, size_t& out_size)
    {
        this->Calculate(in_data, out_size);
    }

    /* 入力データをシリアライズしてアーカイブに変換 */
    template <typename T>
    void Serialize(const T& in_data, Archive& out_archive)
    {
        /* シリアライズ後のデータサイズ */
        size_t size = 0;

        /* シリアライズ後のデータサイズ算出 */
        this->CalculateSize(in_data, size);

        /* シリアライズ後のデータサイズでアーカイブのデータバッファメモリ確保 */
        out_archive.Reserve(size);

        /* オフセット初期化 */
        size_t offset = 0;

        /* シリアライズ */
        this->Serialize(in_data, offset, out_archive);
    }

    /* アーカイブをデシリアライズして出力データに変換 */
    template <typename T>
    void Deserialize(const Archive& in_archive, T& out_data)
    {
        /* オフセット初期化 */
        size_t offset = 0;

        /* デシリアライズ */
        this->Deserialize(in_archive, offset, out_data);
    }

CalculateSize()では指定されたデータ型に応じたデータサイズを算出するCalculate()を呼び出します。構造体型などの場合は各メンバに対してCalculate()を呼び出し、トータルのデータサイズを算出します。

Serialize()ではCalculateSize()を呼び出してシリアライズ後のデータサイズを算出し、アーカイブReserve()を呼び出してデータサイズ分のメモリを確保します。その後、指定されたデータ型に応じたシリアライズ(バイナリ形式に変換し、アーカイブへの書き込み)を行うSerialize()を呼び出します。構造体型などの場合は各メンバに対してSerialize()を呼び出し、すべてのデータをシリアライズします。

Deserialize()では、指定されたデータ型に応じたDeserialize()を呼び出してデシリアライズ(アーカイブからバイナリデータを呼び出し、元のデータに復元)します。構造体型などの場合は各メンバに対してDeserialize()を呼び出し、すべてのメンバをデシリアライズします。


内部関数

プライベートメンバ関数としては、データ型に応じたCalculate()Serialize()Deserialize()を実装します。
データ型に応じた処理内容を詳しく説明していきます。


算術型

算術型(論理型、整数型、浮動小数型)では、そのままバイナリ(バイト配列)との変換が可能なため、バイナリ(バイト配列)と相互変換するための処理を実装します。

/* 算術型データサイズ算出 */
template<typename T, type_traits::concept_t<std::is_arithmetic<T>::value> = nullptr>
void Calculate(const T& in_data, size_t& out_size)
{
    /* 入力データの型のサイズを加算 */
    out_size += sizeof(in_data);
}

/* 算術型シリアライズ */
template<typename T, type_traits::concept_t<std::is_arithmetic<T>::value> = nullptr>
void Serialize(const T& in_data, size_t& offset, Archive& out_archive)
{
    /* バイト配列 */
    std::array<byte_t, sizeof(T)> in_bytes{};

    /* 入力データをバイト配列に変換(エンディアンにより必要に応じてスワップ) */
    binary::BinaryConverter::Convert(this->m_CurrentEndian, this->m_TargetEndian, in_data, in_bytes);

    /* 指定オフセット位置からバイト配列をアーカイブに書き込み */
    out_archive.Write(in_bytes, offset);
}

/* 算術型デシリアライズ */
template<typename T, type_traits::concept_t<std::is_arithmetic<T>::value> = nullptr>
void Deserialize(const Archive& in_archive, size_t& offset, T& out_data)
{
    /* バイト配列 */
    std::array<byte_t, sizeof(T)> out_bytes{};

    /* アーカイブの指定オフセット位置からバイト配列読み込み */
    in_archive.Read(out_bytes, offset);

    /* バイト配列を出力データに変換(エンディアンにより必要に応じてスワップ) */
    binary::BinaryConverter::Convert(this->m_CurrentEndian, this->m_TargetEndian, out_bytes, out_data);
}

Calculate()では指定されたデータ型のサイズをsizeofで算出し、加算します。
Serialize()ではbinary::BinaryConverter::Convert()エンディアンを考慮したバイト配列に変換し、アーカイブWrite()を呼び出してバイト配列を書き込みます。
Deserialize()では、アーカイブRead()を呼び出してバイト配列にバイナリデータを読み込み、binary::BinaryConverter::Convert()エンディアンを考慮して指定されたデータ型に復元します。
binary::BinaryConverter::Convert()の詳細についてはGitHubのソースをご参照ください。


列挙型

列挙型(特にenum class)では、直接バイナリ形式に変換できないため、基底型に変換して処理を行うようにしています。

/* 列挙型(enum/enum class)データサイズ算出 */
template<typename T, type_traits::concept_t<std::is_enum<T>::value> = nullptr>
void Calculate(const T& in_data, size_t& out_size)
{
    /* 入力データの基底型のサイズを加算 */
    this->Calculate(type_traits::underlying_cast(in_data), out_size);
}

/* 列挙型(enum/enum class)シリアライズ */
template<typename T, type_traits::concept_t<std::is_enum<T>::value> = nullptr>
void Serialize(const T& in_data, size_t& offset, Archive& out_archive)
{
    /* 入力データの基底型にキャストしてシリアライズ */
    this->Serialize(type_traits::underlying_cast(in_data), offset, out_archive);
}

/* 列挙型(enum/enum class)デシリアライズ */
template<typename T, type_traits::concept_t<std::is_enum<T>::value> = nullptr>
void Deserialize(const Archive& in_archive, size_t& offset, T& out_data)
{
    /* 入力データの基底型データ */
    type_traits::underlying_type_t<T> data{};

    /* 入力データの基底型としてデシリアライズ */
    this->Deserialize(in_archive, offset, data);

    /* 入力データの型にキャストしてセット */
    out_data = static_cast<T>(data);
}

Calculate()およびSerialize()では、type_traits::underlying_castを用いて基底型(デフォルトではint)に変換してCalculate()Serialize()を呼び出しています。
Deserialize()では、type_traits::underlying_type_t<T>で基底型の一時変数を用意し、基底型に対するDeserialize()を呼び出してから元のデータ型にstatic_castで変換しています。
type_traits::underlying_castおよびtype_traits::underlying_type_t<T>の詳細についてはGitHubのソースをご参照ください。


STLコンテナ型

STLコンテナ(std::arraystd::vectorstd::pairstd::mapなど)に対しては、要素の数と格納されている各要素に対するシリアライズ/デシリアライズ処理を行います。

/* std::array型データサイズ算出 */
template <typename T, size_t N>
void Calculate(const std::array<T, N>& in_data, size_t& out_size)
{
    /* 要素数の型(uint64_t型)のデータサイズを加算 */
    out_size += sizeof(uint64_t);

    /* 全要素を走査 */
    for (const auto& item : in_data)
    {
        /* 各要素のデータサイズを算出して加算 */
        this->Calculate(item, out_size);
    }
}

/* std::array型シリアライズ */
template <typename T, size_t N>
void Serialize(const std::array<T, N>& in_data, size_t& offset, Archive& out_archive)
{
    /* 要素数をシリアライズ */
    this->Serialize(static_cast<uint64_t>(in_data.size()), offset, out_archive);

    /* 全要素を走査 */
    for (const auto& item : in_data)
    {
        /* 各要素をシリアライズ */
        this->Serialize(item, offset, out_archive);
    }
}

/* std::array型デシリアライズ */
template <typename T, size_t N>
void Deserialize(const Archive& in_archive, size_t& offset, std::array<T, N>& out_data)
{
    /* 要素数 */
    uint64_t size = 0;

    /* 要素数をデシリアライズ */
    this->Deserialize(in_archive, offset, size);

    /* 要素数分ループ */
    for (uint64_t i = 0; i < size; i++)
    {
        /* 要素 */
        T item{};

        /* 各要素をデシリアライズ */
        this->Deserialize(in_archive, offset, item);

        /* 要素をセット */
        out_data[i] = item;
    }
}


/* std::vector型データサイズ算出 */
template <typename T>
void Calculate(const std::vector<T>& in_data, size_t& out_size)
{
    /* 要素数の型(uint64_t型)のデータサイズを加算 */
    out_size += sizeof(uint64_t);

    /* 全要素を走査 */
    for (const auto& item : in_data)
    {
        /* 各要素のデータサイズを算出して加算 */
        this->Calculate(item, out_size);
    }
}

/* std::array型シリアライズ */
template <typename T>
void Serialize(const std::vector<T>& in_data, size_t& offset, Archive& out_archive)
{
    /* 要素数をシリアライズ */
    this->Serialize(static_cast<uint64_t>(in_data.size()), offset, out_archive);

    /* 全要素を走査 */
    for (const auto& item : in_data)
    {
        /* 各要素をシリアライズ */
        this->Serialize(item, offset, out_archive);
    }
}

/* std::vector型デシリアライズ */
template <typename T>
void Deserialize(const Archive& in_archive, size_t& offset, std::vector<T>& out_data)
{
    /* 要素数 */
    uint64_t size = 0;

    /* 要素数をデシリアライズ */
    this->Deserialize(in_archive, offset, size);

    /* 要素数分メモリを事前確保 */
    out_data.reserve(size);

    /* 要素数分ループ */
    for (uint64_t i = 0; i < size; i++)
    {
        /* 要素 */
        T item{};

        /* 各要素をデシリアライズ */
        this->Deserialize(in_archive, offset, item);

        /* 要素を追加 */
        out_data.push_back(item);
    }
}


/* std::pair型データサイズ算出 */
template <typename T1, typename T2>
void Calculate(const std::pair<T1, T2>& in_data, size_t& out_size)
{
    /* 1st要素データサイズ算出 */
    this->Calculate(in_data.first, out_size);

    /* 2nd要素データサイズ算出 */
    this->Calculate(in_data.second, out_size);
}

/* std::pair型シリアライズ */
template <typename T1, typename T2>
void Serialize(const std::pair<T1, T2>& in_data, size_t& offset, Archive& out_archive)
{
    /* 1st要素シリアライズ */
    this->Serialize(in_data.first, offset, out_archive);

    /* 2nd要素シリアライズ */
    this->Serialize(in_data.second, offset, out_archive);
}

/* std::pair型デシリアライズ */
template <typename T1, typename T2>
void Deserialize(const Archive& in_archive, size_t& offset, std::pair<T1, T2>& out_data)
{
    /* 1st要素 */
    T1 first{};

    /* 1st要素デシリアライズ */
    this->Deserialize(in_archive, offset, first);

    /* 2nd要素 */
    T2 second{};

    /* 2nd要素デシリアライズ */
    this->Deserialize(in_archive, offset, second);

    /* std::pairに変換してセット */
    out_data = std::make_pair<T1, T2>(first, second);
}


/* std::map型データサイズ算出 */
template <typename TKey, typename TValue>
void Calculate(const std::map<TKey, TValue>& in_data, size_t& out_size)
{
    /* 要素数の型(uint64_t型)のデータサイズを加算 */
    out_size += sizeof(uint64_t);

    /* 全要素を走査 */
    for (const auto& item : in_data)
    {
        /* 各要素のデータサイズを算出して加算 */
        this->Calculate(item, out_size);
    }
}

/* std::map型シリアライズ */
template <typename TKey, typename TValue>
void Serialize(const std::map<TKey, TValue>& in_data, size_t& offset, Archive& out_archive)
{
    /* 要素数をシリアライズ */
    this->Serialize(in_data.size(), offset, out_archive);

    /* 全要素を走査 */
    for (const auto& item : in_data)
    {
        /* 各要素をシリアライズ */
        this->Serialize(item, offset, out_archive);
    }
}

/* std::map型デシリアライズ */
template <typename TKey, typename TValue>
void Deserialize(const Archive& in_archive, size_t& offset, std::map<TKey, TValue>& out_data)
{
    /* 要素数 */
    uint64_t size = 0;

    /* 要素数をデシリアライズ */
    this->Deserialize(in_archive, offset, size);

    /* 要素数分ループ */
    for (uint64_t i = 0; i < size; i++)
    {
        /* キー要素 */
        TKey key{};

        /* キー要素をデシリアライズ */
        this->Deserialize(in_archive, offset, key);

        /* 値要素 */
        TValue value{};

        /* 値要素をデシリアライズ */
        this->Deserialize(in_archive, offset, value);

        /* キー/値を追加 */
        out_data.emplace(std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(value));
    }
}

例としてstd::arrayに対するシリアライズ/デシリアライズ処理について説明します。

Calculate()では、size()で取得できる要素の数に対するデータサイズを加算し、その後各要素に対してデータサイズを算出するCalculate()を呼び出します。
size()取得できるデータサイズの型はsize_t型ですが、size_t型は環境により型のサイズが異なるため、uint64_t型で加算しています。

Serialize()では、size()で取得した要素数uint64_t型にキャストしてSerialize()を呼び出すことで要素数シリアライズし、その後各要素に対してシリアライズを行うSerialize()を呼び出します。

Deserialize()では、最初に要素数をデシリアライズ(uint64_t型としてDeserialize()を呼び出し)し、要素数Deserialize()を呼び出して各要素をデシリアライズします。

std::vectorstd::mapも同様に実装しています。

std::pairはメンバのfirstsecondに対してそれぞれCalculate()Serialize()Deserialize()を行うようにしています。


文字列型

文字列型(std::string)も基本的にはSTLstd::vectorと同様の方法で実装していますが、文字列の場合は、終端にnull文字('\0')を追加するようにしています。
そのため、要素数にもnull文字分(char型)のサイズを加算しています。

/* 文字列型データサイズ算出 */
inline void Calculate(const string_t& in_data, size_t& out_size)
{
    /* 要素数の型(uint64_t型)のデータサイズを加算 */
    out_size += sizeof(uint64_t);

    /* 要素数算出(文字列の長さ + null文字のサイズ) */
    uint64_t data_size = static_cast<uint64_t>(in_data.size() + sizeof(char));

    /* 要素数を加算 */
    out_size += data_size;
}

/* 文字列型シリアライズ */
inline void Serialize(const string_t& in_data, size_t& offset, Archive& out_archive)
{
    /* 要素数算出(文字列の長さ + null文字のサイズ) */
    uint64_t data_size = static_cast<uint64_t>(in_data.size() + sizeof(char));

    /* 要素数をシリアライズ */
    this->Serialize(data_size, offset, out_archive);

    /* 指定オフセット位置から文字列データ書き込み */
    out_archive.Write(in_data, offset);
}

/* 文字列型デシリアライズ */
inline void Deserialize(const Archive& in_archive, size_t& offset, string_t& out_data)
{
    /* 要素数 */
    uint64_t data_size = 0;

    /* 要素数をデシリアライズ */
    this->Deserialize(in_archive, offset, data_size);

    /* 指定オフセット位置から指定サイズ分文字列データ読み込み */
    in_archive.Read(out_data, offset, static_cast<size_t>(data_size));
}


クラス/構造体型

ここが今回肝となる部分の実装です。
任意のクラス/構造体のシリアライズ処理を行うためには、メンバ変数を列挙して各メンバに対してCalculate()Serialize()Deserialize()を呼び出す必要があります。

C#などであれば、リフレクションによってメンバ変数を列挙することが可能ですが、C++ではそういった機能は実装されていないので、疑似的にメンバ変数を列挙する処理を実装する必要があります。

今回は、クラス/構造体型のメンバ変数をタプル(std::tuple)型で列挙する関数を定義し、そのタプルに対してSwallowイディオムおよびIndex Tupleイディオムを用いて可変長テンプレートのテンプレートパラメータパックを展開する方法を用いています。

SwallowイディオムおよびIndex Tupleイディオムに関しては、下記のページで詳しく説明されています。
C++魔術回路構成法

/* クラス/構造体(class/struct)型メンバデータサイズ算出 */
template <typename TUPLE, size_t ...I>
void CalculateTupleImple(size_t& out_size, TUPLE&& t, std::index_sequence<I...>)
{
    /* swallowイディオムでクラス/構造体(class/struct)型メンバのタプルを順次展開し、データサイズ算出 */
    using swallow = std::initializer_list<int>;

    (void)swallow
    {
        (void(this->Calculate(std::get<I>(t), out_size)), 0)...
    };
}

/* クラス/構造体(class/struct)型メンバシリアライズ */
template <typename TUPLE, size_t ...I>
void SerializeTupleImple(size_t& offset, Archive& out_archive, TUPLE&& t, std::index_sequence<I...>)
{
    /* swallowイディオムでクラス/構造体(class/struct)型メンバのタプルを順次展開し、シリアライズ */
    using swallow = std::initializer_list<int>;

    (void)swallow
    {
        (void(this->Serialize(std::get<I>(t), offset, out_archive)), 0)...
    };
}

/* クラス/構造体(class/struct)型メンバデシリアライズ */
template <typename TUPLE, size_t ...I>
void DeserializeTupleImple(const Archive& in_archive, size_t& offset, TUPLE&& t, std::index_sequence<I...>)
{
    /* swallowイディオムでクラス/構造体(class/struct)型メンバのタプルを順次展開し、デシリアライズ */
    using swallow = std::initializer_list<int>;

    (void)swallow
    {
        (void(this->Deserialize(in_archive, offset, std::get<I>(t))), 0)...
    };
}


/* クラス/構造体(class/struct)型メンバタプルデータサイズ算出 */
template <typename TUPLE>
void CalculateTuple(size_t& out_size, TUPLE&& t)
{
    /* クラス/構造体(class/struct)型メンバデータサイズ算出 */
    this->CalculateTupleImple(out_size, std::forward<TUPLE>(t), std::make_index_sequence<std::tuple_size<std::decay_t<TUPLE>>::value>{});
}

/* クラス/構造体(class/struct)型メンバタプルシリアライズ */
template <typename TUPLE>
void SerializeTuple(size_t& offset, Archive& out_archive, TUPLE&& t)
{
    /* クラス/構造体(class/struct)型メンバシリアライズ */
    this->SerializeTupleImple(offset, out_archive, std::forward<TUPLE>(t), std::make_index_sequence<std::tuple_size<std::decay_t<TUPLE>>::value>{});
}

/* クラス/構造体(class/struct)型メンバタプルデシリアライズ */
template <typename TUPLE>
void DeserializeTuple(const Archive& in_archive, size_t& offset, TUPLE&& t)
{
    /* クラス/構造体(class/struct)型メンバデシリアライズ */
    this->DeserializeTupleImple(in_archive, offset, std::forward<TUPLE>(t), std::make_index_sequence<std::tuple_size<std::decay_t<TUPLE>>::value>{});
}

/* クラス/構造体(class/struct)型データサイズ算出 */
template<class T>
void Calculate(const T& in_data, size_t& out_size, typename std::enable_if<std::is_class<T>::value>::type* = nullptr)
{
    /* メンバのタプル取得 */
    auto tuple = type_traits::DataTypeTraits<T>::GetMembersAsTuple(in_data);

    /* メンバのタプルから各メンバのデータサイズ算出 */
    this->CalculateTuple(out_size, tuple);
}

/* クラス/構造体(class/struct)型シリアライズ */
template <class T>
void Serialize(const T& in_data, size_t& offset, Archive& out_archive, typename std::enable_if<std::is_class<T>::value>::type* = nullptr)
{
    /* メンバのタプル取得 */
    auto tuple = type_traits::DataTypeTraits<T>::GetMembersAsTuple(in_data);

    /* メンバのタプルから各メンバをシリアライズ */
    this->SerializeTuple(offset, out_archive, tuple);
}

/* クラス/構造体(class/struct)型デシリアライズ */
template <class T>
void Deserialize(const Archive& in_archive, size_t& offset, T& out_data, typename std::enable_if<std::is_class<T>::value>::type* = nullptr)
{
    /* メンバのタプル取得 */
    auto tuple = type_traits::DataTypeTraits<T>::GetMembersAsTuple(out_data);

    /* メンバのタプルから各メンバをデシリアライズ */
    this->DeserializeTuple(in_archive, offset, tuple);
}

Calculate()を例に説明します。
Calculate()では、type_traits::DataTypeTraits<T>::GetMembersAsTuple()を呼び出すことで、入力データのメンバをタプル(std::tuple)型で取得し、CalculateTuple()に渡しています。
CalculateTuple()ではstd::forwardでタプルデータを渡し、またstd::make_index_sequence<std::tuple_size<std::decay_t<TUPLE>>::value>{}でタプルの要素数分のインデックスシーケンスを生成して渡しています。
さらにCalculateTupleImple()では、渡されたタプルと要素のインデックスにより指定インデックス位置のメンバ変数を取り出し、メンバの型に対応したCalculate()を呼び出しています。
タプル要素インデックス(I)は可変長テンプレートパラメータとなっており、SwallowイディオムおよびIndex Tupleイディオムによって順次展開されるため、すべてのタプルに含まれるすべてのメンバに対して順番にCalculate()が呼ばれる仕組みになっています。

type_traits::DataTypeTraits<T>::GetMembersAsTuple()の定義方法は後ほど使い方の項で説明します。


ソース全体は以下の通りです。

バイナリ形式シリアライズクラス

/* バイナリ形式シリアライズクラス */
class BinarySerialization
{
public:
    /* シングルトンインスタンス取得 */
    static BinarySerialization& GetInstance()
    {
        static BinarySerialization instance;
        return instance;
    }

private:
    /* コンストラクタ */
    BinarySerialization()
        : m_CurrentEndian(endian::GetEnvironmentEndian())
        , m_TargetEndian(endian::EndianType::Network)
    {
        /* Nothing to do */
    }

    /* デストラクタ */
    ~BinarySerialization() = default;

public:
    /* コピー&ムーブセマンティクス無効化 */
    BinarySerialization(const BinarySerialization&) = delete;
    BinarySerialization(BinarySerialization&&) = delete;
    BinarySerialization& operator=(const BinarySerialization&) = delete;
    BinarySerialization& operator=(BinarySerialization&&) = delete;

public:
    /* シリアライズ後のデータサイズ算出 */
    template <typename T>
    void CalculateSize(const T& in_data, size_t& out_size)
    {
        this->Calculate(in_data, out_size);
    }

    /* 入力データをシリアライズしてアーカイブに変換 */
    template <typename T>
    void Serialize(const T& in_data, Archive& out_archive)
    {
        /* シリアライズ後のデータサイズ */
        size_t size = 0;

        /* シリアライズ後のデータサイズ算出 */
        this->CalculateSize(in_data, size);

        /* シリアライズ後のデータサイズでアーカイブのデータバッファメモリ確保 */
        out_archive.Reserve(size);

        /* オフセット初期化 */
        size_t offset = 0;

        /* シリアライズ */
        this->Serialize(in_data, offset, out_archive);
    }

    /* アーカイブをデシリアライズして出力データに変換 */
    template <typename T>
    void Deserialize(const Archive& in_archive, T& out_data)
    {
        /* オフセット初期化 */
        size_t offset = 0;

        /* デシリアライズ */
        this->Deserialize(in_archive, offset, out_data);
    }

private:
    /* 算術型データサイズ算出 */
    template<typename T, type_traits::concept_t<std::is_arithmetic<T>::value> = nullptr>
    void Calculate(const T& in_data, size_t& out_size)
    {
        /* 入力データの型のサイズを加算 */
        out_size += sizeof(in_data);
    }

    /* 算術型シリアライズ */
    template<typename T, type_traits::concept_t<std::is_arithmetic<T>::value> = nullptr>
    void Serialize(const T& in_data, size_t& offset, Archive& out_archive)
    {
        /* バイト配列 */
        std::array<byte_t, sizeof(T)> in_bytes{};

        /* 入力データをバイト配列に変換(エンディアンにより必要に応じてスワップ) */
        binary::BinaryConverter::Convert(this->m_CurrentEndian, this->m_TargetEndian, in_data, in_bytes);

        /* 指定オフセット位置からバイト配列をアーカイブに書き込み */
        out_archive.Write(in_bytes, offset);
    }

    /* 算術型デシリアライズ */
    template<typename T, type_traits::concept_t<std::is_arithmetic<T>::value> = nullptr>
    void Deserialize(const Archive& in_archive, size_t& offset, T& out_data)
    {
        /* バイト配列 */
        std::array<byte_t, sizeof(T)> out_bytes{};

        /* アーカイブの指定オフセット位置からバイト配列読み込み */
        in_archive.Read(out_bytes, offset);

        /* バイト配列を出力データに変換(エンディアンにより必要に応じてスワップ) */
        binary::BinaryConverter::Convert(this->m_CurrentEndian, this->m_TargetEndian, out_bytes, out_data);
    }

    /* 列挙型(enum/enum class)データサイズ算出 */
    template<typename T, type_traits::concept_t<std::is_enum<T>::value> = nullptr>
    void Calculate(const T& in_data, size_t& out_size)
    {
        /* 入力データの基底型のサイズを加算 */
        this->Calculate(type_traits::underlying_cast(in_data), out_size);
    }

    /* 列挙型(enum/enum class)シリアライズ */
    template<typename T, type_traits::concept_t<std::is_enum<T>::value> = nullptr>
    void Serialize(const T& in_data, size_t& offset, Archive& out_archive)
    {
        /* 入力データの基底型にキャストしてシリアライズ */
        this->Serialize(type_traits::underlying_cast(in_data), offset, out_archive);
    }

    /* 列挙型(enum/enum class)デシリアライズ */
    template<typename T, type_traits::concept_t<std::is_enum<T>::value> = nullptr>
    void Deserialize(const Archive& in_archive, size_t& offset, T& out_data)
    {
        /* 入力データの基底型データ */
        type_traits::underlying_type_t<T> data{};

        /* 入力データの基底型としてデシリアライズ */
        this->Deserialize(in_archive, offset, data);

        /* 入力データの型にキャストしてセット */
        out_data = static_cast<T>(data);
    }


    /* std::array型データサイズ算出 */
    template <typename T, size_t N>
    void Calculate(const std::array<T, N>& in_data, size_t& out_size)
    {
        /* 要素数の型(uint64_t型)のデータサイズを加算 */
        out_size += sizeof(uint64_t);

        /* 全要素を走査 */
        for (const auto& item : in_data)
        {
            /* 各要素のデータサイズを算出して加算 */
            this->Calculate(item, out_size);
        }
    }

    /* std::array型シリアライズ */
    template <typename T, size_t N>
    void Serialize(const std::array<T, N>& in_data, size_t& offset, Archive& out_archive)
    {
        /* 要素数をシリアライズ */
        this->Serialize(static_cast<uint64_t>(in_data.size()), offset, out_archive);

        /* 全要素を走査 */
        for (const auto& item : in_data)
        {
            /* 各要素をシリアライズ */
            this->Serialize(item, offset, out_archive);
        }
    }

    /* std::array型デシリアライズ */
    template <typename T, size_t N>
    void Deserialize(const Archive& in_archive, size_t& offset, std::array<T, N>& out_data)
    {
        /* 要素数 */
        uint64_t size = 0;

        /* 要素数をデシリアライズ */
        this->Deserialize(in_archive, offset, size);

        /* 要素数分ループ */
        for (uint64_t i = 0; i < size; i++)
        {
            /* 要素 */
            T item{};

            /* 各要素をデシリアライズ */
            this->Deserialize(in_archive, offset, item);

            /* 要素をセット */
            out_data[i] = item;
        }
    }


    /* std::vector型データサイズ算出 */
    template <typename T>
    void Calculate(const std::vector<T>& in_data, size_t& out_size)
    {
        /* 要素数の型(uint64_t型)のデータサイズを加算 */
        out_size += sizeof(uint64_t);

        /* 全要素を走査 */
        for (const auto& item : in_data)
        {
            /* 各要素のデータサイズを算出して加算 */
            this->Calculate(item, out_size);
        }
    }

    /* std::array型シリアライズ */
    template <typename T>
    void Serialize(const std::vector<T>& in_data, size_t& offset, Archive& out_archive)
    {
        /* 要素数をシリアライズ */
        this->Serialize(static_cast<uint64_t>(in_data.size()), offset, out_archive);

        /* 全要素を走査 */
        for (const auto& item : in_data)
        {
            /* 各要素をシリアライズ */
            this->Serialize(item, offset, out_archive);
        }
    }

    /* std::vector型デシリアライズ */
    template <typename T>
    void Deserialize(const Archive& in_archive, size_t& offset, std::vector<T>& out_data)
    {
        /* 要素数 */
        uint64_t size = 0;

        /* 要素数をデシリアライズ */
        this->Deserialize(in_archive, offset, size);

        /* 要素数分メモリを事前確保 */
        out_data.reserve(size);

        /* 要素数分ループ */
        for (uint64_t i = 0; i < size; i++)
        {
            /* 要素 */
            T item{};

            /* 各要素をデシリアライズ */
            this->Deserialize(in_archive, offset, item);

            /* 要素を追加 */
            out_data.push_back(item);
        }
    }


    /* std::pair型データサイズ算出 */
    template <typename T1, typename T2>
    void Calculate(const std::pair<T1, T2>& in_data, size_t& out_size)
    {
        /* 1st要素データサイズ算出 */
        this->Calculate(in_data.first, out_size);

        /* 2nd要素データサイズ算出 */
        this->Calculate(in_data.second, out_size);
    }

    /* std::pair型シリアライズ */
    template <typename T1, typename T2>
    void Serialize(const std::pair<T1, T2>& in_data, size_t& offset, Archive& out_archive)
    {
        /* 1st要素シリアライズ */
        this->Serialize(in_data.first, offset, out_archive);

        /* 2nd要素シリアライズ */
        this->Serialize(in_data.second, offset, out_archive);
    }

    /* std::pair型デシリアライズ */
    template <typename T1, typename T2>
    void Deserialize(const Archive& in_archive, size_t& offset, std::pair<T1, T2>& out_data)
    {
        /* 1st要素 */
        T1 first{};

        /* 1st要素デシリアライズ */
        this->Deserialize(in_archive, offset, first);

        /* 2nd要素 */
        T2 second{};

        /* 2nd要素デシリアライズ */
        this->Deserialize(in_archive, offset, second);

        /* std::pairに変換してセット */
        out_data = std::make_pair<T1, T2>(first, second);
    }


    /* std::map型データサイズ算出 */
    template <typename TKey, typename TValue>
    void Calculate(const std::map<TKey, TValue>& in_data, size_t& out_size)
    {
        /* 要素数の型(uint64_t型)のデータサイズを加算 */
        out_size += sizeof(uint64_t);

        /* 全要素を走査 */
        for (const auto& item : in_data)
        {
            /* 各要素のデータサイズを算出して加算 */
            this->Calculate(item, out_size);
        }
    }

    /* std::map型シリアライズ */
    template <typename TKey, typename TValue>
    void Serialize(const std::map<TKey, TValue>& in_data, size_t& offset, Archive& out_archive)
    {
        /* 要素数をシリアライズ */
        this->Serialize(in_data.size(), offset, out_archive);

        /* 全要素を走査 */
        for (const auto& item : in_data)
        {
            /* 各要素をシリアライズ */
            this->Serialize(item, offset, out_archive);
        }
    }

    /* std::map型デシリアライズ */
    template <typename TKey, typename TValue>
    void Deserialize(const Archive& in_archive, size_t& offset, std::map<TKey, TValue>& out_data)
    {
        /* 要素数 */
        uint64_t size = 0;

        /* 要素数をデシリアライズ */
        this->Deserialize(in_archive, offset, size);

        /* 要素数分ループ */
        for (uint64_t i = 0; i < size; i++)
        {
            /* キー要素 */
            TKey key{};

            /* キー要素をデシリアライズ */
            this->Deserialize(in_archive, offset, key);

            /* 値要素 */
            TValue value{};

            /* 値要素をデシリアライズ */
            this->Deserialize(in_archive, offset, value);

            /* キー/値を追加 */
            out_data.emplace(std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(value));
        }
    }


    /* クラス/構造体(class/struct)型メンバデータサイズ算出 */
    template <typename TUPLE, size_t ...I>
    void CalculateTupleImple(size_t& out_size, TUPLE&& t, std::index_sequence<I...>)
    {
        /* swallowイディオムでクラス/構造体(class/struct)型メンバのタプルを順次展開し、データサイズ算出 */
        using swallow = std::initializer_list<int>;

        (void)swallow
        {
            (void(this->Calculate(std::get<I>(t), out_size)), 0)...
        };
    }

    /* クラス/構造体(class/struct)型メンバシリアライズ */
    template <typename TUPLE, size_t ...I>
    void SerializeTupleImple(size_t& offset, Archive& out_archive, TUPLE&& t, std::index_sequence<I...>)
    {
        /* swallowイディオムでクラス/構造体(class/struct)型メンバのタプルを順次展開し、シリアライズ */
        using swallow = std::initializer_list<int>;

        (void)swallow
        {
            (void(this->Serialize(std::get<I>(t), offset, out_archive)), 0)...
        };
    }

    /* クラス/構造体(class/struct)型メンバデシリアライズ */
    template <typename TUPLE, size_t ...I>
    void DeserializeTupleImple(const Archive& in_archive, size_t& offset, TUPLE&& t, std::index_sequence<I...>)
    {
        /* swallowイディオムでクラス/構造体(class/struct)型メンバのタプルを順次展開し、デシリアライズ */
        using swallow = std::initializer_list<int>;

        (void)swallow
        {
            (void(this->Deserialize(in_archive, offset, std::get<I>(t))), 0)...
        };
    }


    /* クラス/構造体(class/struct)型メンバタプルデータサイズ算出 */
    template <typename TUPLE>
    void CalculateTuple(size_t& out_size, TUPLE&& t)
    {
        /* クラス/構造体(class/struct)型メンバデータサイズ算出 */
        this->CalculateTupleImple(out_size, std::forward<TUPLE>(t), std::make_index_sequence<std::tuple_size<std::decay_t<TUPLE>>::value>{});
    }

    /* クラス/構造体(class/struct)型メンバタプルシリアライズ */
    template <typename TUPLE>
    void SerializeTuple(size_t& offset, Archive& out_archive, TUPLE&& t)
    {
        /* クラス/構造体(class/struct)型メンバシリアライズ */
        this->SerializeTupleImple(offset, out_archive, std::forward<TUPLE>(t), std::make_index_sequence<std::tuple_size<std::decay_t<TUPLE>>::value>{});
    }

    /* クラス/構造体(class/struct)型メンバタプルデシリアライズ */
    template <typename TUPLE>
    void DeserializeTuple(const Archive& in_archive, size_t& offset, TUPLE&& t)
    {
        /* クラス/構造体(class/struct)型メンバデシリアライズ */
        this->DeserializeTupleImple(in_archive, offset, std::forward<TUPLE>(t), std::make_index_sequence<std::tuple_size<std::decay_t<TUPLE>>::value>{});
    }

    /* クラス/構造体(class/struct)型データサイズ算出 */
    template<class T>
    void Calculate(const T& in_data, size_t& out_size, typename std::enable_if<std::is_class<T>::value>::type* = nullptr)
    {
        /* メンバのタプル取得 */
        auto tuple = type_traits::DataTypeTraits<T>::GetMembersAsTuple(in_data);

        /* メンバのタプルから各メンバのデータサイズ算出 */
        this->CalculateTuple(out_size, tuple);
    }

    /* クラス/構造体(class/struct)型シリアライズ */
    template <class T>
    void Serialize(const T& in_data, size_t& offset, Archive& out_archive, typename std::enable_if<std::is_class<T>::value>::type* = nullptr)
    {
        /* メンバのタプル取得 */
        auto tuple = type_traits::DataTypeTraits<T>::GetMembersAsTuple(in_data);

        /* メンバのタプルから各メンバをシリアライズ */
        this->SerializeTuple(offset, out_archive, tuple);
    }

    /* クラス/構造体(class/struct)型デシリアライズ */
    template <class T>
    void Deserialize(const Archive& in_archive, size_t& offset, T& out_data, typename std::enable_if<std::is_class<T>::value>::type* = nullptr)
    {
        /* メンバのタプル取得 */
        auto tuple = type_traits::DataTypeTraits<T>::GetMembersAsTuple(out_data);

        /* メンバのタプルから各メンバをデシリアライズ */
        this->DeserializeTuple(in_archive, offset, tuple);
    }


    /* 文字列型データサイズ算出 */
    inline void Calculate(const string_t& in_data, size_t& out_size)
    {
        /* 要素数の型(uint64_t型)のデータサイズを加算 */
        out_size += sizeof(uint64_t);

        /* 要素数算出(文字列の長さ + null文字のサイズ) */
        uint64_t data_size = static_cast<uint64_t>(in_data.size() + sizeof(char));

        /* 要素数を加算 */
        out_size += data_size;
    }

    /* 文字列型シリアライズ */
    inline void Serialize(const string_t& in_data, size_t& offset, Archive& out_archive)
    {
        /* 要素数算出(文字列の長さ + null文字のサイズ) */
        uint64_t data_size = static_cast<uint64_t>(in_data.size() + sizeof(char));

        /* 要素数をシリアライズ */
        this->Serialize(data_size, offset, out_archive);

        /* 指定オフセット位置から文字列データ書き込み */
        out_archive.Write(in_data, offset);
    }

    /* 文字列型デシリアライズ */
    inline void Deserialize(const Archive& in_archive, size_t& offset, string_t& out_data)
    {
        /* 要素数 */
        uint64_t data_size = 0;

        /* 要素数をデシリアライズ */
        this->Deserialize(in_archive, offset, data_size);

        /* 指定オフセット位置から指定サイズ分文字列データ読み込み */
        in_archive.Read(out_data, offset, static_cast<size_t>(data_size));
    }

private:
    /* 現在の環境のエンディアン */
    endian::EndianType m_CurrentEndian;

    /* シリアライズ後のエンディアン */
    endian::EndianType m_TargetEndian;
};

あとがき

今回はバイナリ形式でのシリアライズ処理を実装しました。
次回は、テキスト形式でのシリアライズ処理の実装について説明します。