An Embedded Engineer’s Blog

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

C++で任意のデータ型をシリアライズ - その4 使い方編

まえがき

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

今回は前回に引き続き任意のデータ型(クラス/構造体)に対してシリアライズできるようにする方法について説明します。

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


前提条件

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

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


実装

ユーザデータ型定義

まずは、ユーザが使用する任意のデータ型を定義します。
今回はサンプルとして、プリミティブ型、STLコンテナ型、列挙型、構造体型でそれぞれデータ型を定義しています。

/* 論理型 */
using BoolMessage = bool_t;

/* 符号付整数型(8bit) */
using Int8Message = int8_t;

/* 符号付整数型(16bit) */
using Int16Message = int16_t;

/* 符号付整数型(32bit) */
using Int32Message = int32_t;

/* 符号付整数型(64bit) */
using Int64Message = int64_t;

/* 符号無整数型(8bit) */
using UInt8Message = uint8_t;

/* 符号無整数型(16bit) */
using UInt16Message = uint16_t;

/* 符号無整数型(32bit) */
using UInt32Message = uint32_t;

/* 符号無整数型(64bit) */
using UInt64Message = uint64_t;

/* 浮動小数型(32bit) */
using Float32Message = float32_t;

/* 浮動小数型(64bit) */
using Float64Message = float64_t;

/* 文字列型 */
using StringMessage = string_t;

/* std::array型 */
using ArrayMessage = std::array<int32_t, 4>;

/* std::vector型 */
using VectorMessage = std::vector<float64_t>;

/* std::map型 */
using MapMessage = std::map<int32_t, float64_t>;

/* 列挙型(enum class) */
enum class EnumMessage1
{
    VALUE1,
    VALUE2,
    VALUE3,
    VALUE4,
};

/* 列挙型(enum) */
enum EnumMessage2
{
    ENUM_MSG2_VALUE1,
    ENUM_MSG2_VALUE2,
    ENUM_MSG2_VALUE3,
    ENUM_MSG2_VALUE4,
    ENUM_MSG2_VALUE5,
};

/* 構造体型1 */
struct StrucMessage1
{
    bool_t bool_value;
    uint32_t uint_value;
    float32_t float_value;
};

/* 構造体型2 */
struct StrucMessage2
{
    std::array<float32_t, 8> float_array;
    std::vector<uint64_t> ulong_vec;
};

/* 列挙型(メンバ用) */
enum class EnumType1
{
    ENUM_VALUE1,
    ENUM_VALUE2,
    ENUM_VALUE3,
};

/* 構造体型3 */
struct StrucMessage3
{
    string_t string_value;
    std::map<EnumType1, string_t> enum_str_map;
};

ユーザデータ型特性定義

リアライザクラスが参照するtype_traits::DataTypeTraits<T>GetMembersAsTuple()およびGetNamedMembersAsTuple()を定義します。

GetMembersAsTuple()では、シリアライズ対象となるすべてのメンバ変数をstd::tieで列挙して返すようにします。
GetNamedMembersAsTuple()では、シリアライズ対象となるメンバ変数とそのメンバ変数名をstd::tieで列挙し、さらにstd::make_tuple()でタプル型として返すようにします。
GetMembersAsTuple()GetNamedMembersAsTuple()のいずれも変更可能なもの(constなし)と、変更不可のもの(const付き)の2種類を定義します。

using namespace cpp_lib::type_traits;

/* app::StrucMessage1 */
template <>
struct DataTypeTraits<app::StrucMessage1>
{
    /* メンバをタプルで取得(変更不可) */
    static auto GetMembersAsTuple(const app::StrucMessage1& data)
    {
        return std::tie(data.bool_value, data.uint_value, data.float_value);
    }

    /* メンバをタプルで取得(変更可) */
    static auto GetMembersAsTuple(app::StrucMessage1& data)
    {
        return std::tie(data.bool_value, data.uint_value, data.float_value);
    }

    /* メンバを名前付きタプルで取得(変更不可) */
    static auto GetNamedMembersAsTuple(const app::StrucMessage1& data)
    {
        return std::make_tuple(std::tie("bool_value", data.bool_value), std::tie("uint_value", data.uint_value), std::tie("float_value", data.float_value));
    }

    /* メンバを名前付きタプルで取得(変更可) */
    static auto GetNamedMembersAsTuple(app::StrucMessage1& data)
    {
        return std::make_tuple(std::tie("bool_value", data.bool_value), std::tie("uint_value", data.uint_value), std::tie("float_value", data.float_value));
    }
};

/* app::StrucMessage2 */
template <>
struct DataTypeTraits<app::StrucMessage2>
{
    /* メンバをタプルで取得(変更不可) */
    static auto GetMembersAsTuple(const app::StrucMessage2& data)
    {
        return std::tie(data.float_array, data.ulong_vec);
    }

    /* メンバをタプルで取得(変更可) */
    static auto GetMembersAsTuple(app::StrucMessage2& data)
    {
        return std::tie(data.float_array, data.ulong_vec);
    }

    /* メンバを名前付きタプルで取得(変更不可) */
    static auto GetNamedMembersAsTuple(const app::StrucMessage2& data)
    {
        return std::make_tuple(std::tie("float_array", data.float_array), std::tie("ulong_vec", data.ulong_vec));
    }

    /* メンバを名前付きタプルで取得(変更可) */
    static auto GetNamedMembersAsTuple(app::StrucMessage2& data)
    {
        return std::make_tuple(std::tie("float_array", data.float_array), std::tie("ulong_vec", data.ulong_vec));
    }
};

/* app::StrucMessage3 */
template <>
struct DataTypeTraits<app::StrucMessage3>
{
    /* メンバをタプルで取得(変更不可) */
    static auto GetMembersAsTuple(const app::StrucMessage3& data)
    {
        return std::tie(data.string_value, data.enum_str_map);
    }

    /* メンバをタプルで取得(変更可) */
    static auto GetMembersAsTuple(app::StrucMessage3& data)
    {
        return std::tie(data.string_value, data.enum_str_map);
    }

    /* メンバを名前付きタプルで取得(変更不可) */
    static auto GetNamedMembersAsTuple(const app::StrucMessage3& data)
    {
        return std::make_tuple(std::tie("string_value", data.string_value), std::tie("enum_str_map", data.enum_str_map));
    }

    /* メンバを名前付きタプルで取得(変更可) */
    static auto GetNamedMembersAsTuple(app::StrucMessage3& data)
    {
        return std::make_tuple(std::tie("string_value", data.string_value), std::tie("enum_str_map", data.enum_str_map));
    }
};

ヘッダインクルード

ユーザデータ型特性を定義したヘッダ、シリアライザ(BinarySerialization、TextSerialization)のヘッダを同時にインクルードします。

これにより、独自で定義したデータ型に対するシリアライズ処理が実現できます。

/* 型定義&型特性定義 */
#include "UserDataTypes.h"
#include "UserDataTypeTraits.h"

/* シリアライザ */
#include "BinarySerialization.h"
#include "TextSerialization.h"
#include "Serializer.h"
#include "SerializerFactory.h"

明示的インスタンス

こちらは必須ではありませんが、テンプレートの明示的インスタンス化を行うことで、ユーザが定義した任意のデータ型に対するインスタンス化の回数を抑制し、プログラムの実行サイズやコンパイル時間を削減することができます。

前述のヘッダインクルードと合わせてUserTypeOperations.h/.cppなどのように1つのヘッダ・ソースにまとめて定義しておくと良いと思います。

明示的インスタンス化宣言(ヘッダ側)

ヘッダ側では明示的インスタンス化宣言を行います。
これにより、どこかで明示的インスタンス化が行われていることが宣言され、ヘッダインクルード側(利用側)でのインスタンス化が抑制されます(コンパイル時間の削減)。

namespace serialization
{
    /* バイナリ形式シリアライズ明示的インスタンス化宣言 */
    /* app::StrucMessage1 */
    extern template void BinarySerialization::Calculate<app::StrucMessage1>(const app::StrucMessage1& in_data, size_t& out_size, typename std::enable_if<std::is_class<app::StrucMessage1>::value>::type*);
    extern template void BinarySerialization::Serialize<app::StrucMessage1>(const app::StrucMessage1& in_data, size_t& offset, serialization::Archive& out_archive, typename std::enable_if<std::is_class<app::StrucMessage1>::value>::type*);
    extern template void BinarySerialization::Deserialize<app::StrucMessage1>(const serialization::Archive& in_archive, size_t& offset, app::StrucMessage1& out_data, typename std::enable_if<std::is_class<app::StrucMessage1>::value>::type*);

    /* app::StrucMessage2 */
    extern template void BinarySerialization::Calculate<app::StrucMessage2>(const app::StrucMessage2& in_data, size_t& out_size, typename std::enable_if<std::is_class<app::StrucMessage2>::value>::type*);
    extern template void BinarySerialization::Serialize<app::StrucMessage2>(const app::StrucMessage2& in_data, size_t& offset, serialization::Archive& out_archive, typename std::enable_if<std::is_class<app::StrucMessage2>::value>::type*);
    extern template void BinarySerialization::Deserialize<app::StrucMessage2>(const serialization::Archive& in_archive, size_t& offset, app::StrucMessage2& out_data, typename std::enable_if<std::is_class<app::StrucMessage2>::value>::type*);

    /* app::StrucMessage3 */
    extern template void BinarySerialization::Calculate<app::StrucMessage3>(const app::StrucMessage3& in_data, size_t& out_size, typename std::enable_if<std::is_class<app::StrucMessage3>::value>::type*);
    extern template void BinarySerialization::Serialize<app::StrucMessage3>(const app::StrucMessage3& in_data, size_t& offset, serialization::Archive& out_archive, typename std::enable_if<std::is_class<app::StrucMessage3>::value>::type*);
    extern template void BinarySerialization::Deserialize<app::StrucMessage3>(const serialization::Archive& in_archive, size_t& offset, app::StrucMessage3& out_data, typename std::enable_if<std::is_class<app::StrucMessage3>::value>::type*);


    /* テキスト形式シリアライズ明示的インスタンス化宣言 */
    /* app::StrucMessage1 */
    extern template void TextSerialization::Serialize<app::StrucMessage1>(const app::StrucMessage1& in_data, Node& out_node, typename std::enable_if<std::is_class<app::StrucMessage1>::value>::type*);
    extern template void TextSerialization::Deserialize<app::StrucMessage1>(const Node& in_node, app::StrucMessage1& out_data, typename std::enable_if<std::is_class<app::StrucMessage1>::value>::type*);

    /* app::StrucMessage2 */
    extern template void TextSerialization::Serialize<app::StrucMessage2>(const app::StrucMessage2& in_data, Node& out_node, typename std::enable_if<std::is_class<app::StrucMessage2>::value>::type*);
    extern template void TextSerialization::Deserialize<app::StrucMessage2>(const Node& in_node, app::StrucMessage2& out_data, typename std::enable_if<std::is_class<app::StrucMessage2>::value>::type*);

    /* app::StrucMessage3 */
    extern template void TextSerialization::Serialize<app::StrucMessage3>(const app::StrucMessage3& in_data, Node& out_node, typename std::enable_if<std::is_class<app::StrucMessage3>::value>::type*);
    extern template void TextSerialization::Deserialize<app::StrucMessage3>(const Node& in_node, app::StrucMessage3& out_data, typename std::enable_if<std::is_class<app::StrucMessage3>::value>::type*);
}

明示的インスタンス化定義(ソース側)

ソース側では明示的インスタンス化定義を行います。
これにより、ユーザが定義した任意のデータ型に対するインスタンス化があらかじめ行われ、シリアライザを利用する(呼び出す)側では、インスタンス化が抑制されます(実行ファイルのサイズ削減)。

namespace serialization
{
    /* バイナリ形式シリアライズ明示的インスタンス化 */
    /* app::StrucMessage1 */
    template void BinarySerialization::Calculate<app::StrucMessage1>(const app::StrucMessage1& in_data, size_t& out_size, typename std::enable_if<std::is_class<app::StrucMessage1>::value>::type*);
    template void BinarySerialization::Serialize<app::StrucMessage1>(const app::StrucMessage1& in_data, size_t& offset, serialization::Archive& out_archive, typename std::enable_if<std::is_class<app::StrucMessage1>::value>::type*);
    template void BinarySerialization::Deserialize<app::StrucMessage1>(const serialization::Archive& in_archive, size_t& offset, app::StrucMessage1& out_data, typename std::enable_if<std::is_class<app::StrucMessage1>::value>::type*);

    /* app::StrucMessage2 */
    template void BinarySerialization::Calculate<app::StrucMessage2>(const app::StrucMessage2& in_data, size_t& out_size, typename std::enable_if<std::is_class<app::StrucMessage2>::value>::type*);
    template void BinarySerialization::Serialize<app::StrucMessage2>(const app::StrucMessage2& in_data, size_t& offset, serialization::Archive& out_archive, typename std::enable_if<std::is_class<app::StrucMessage2>::value>::type*);
    template void BinarySerialization::Deserialize<app::StrucMessage2>(const serialization::Archive& in_archive, size_t& offset, app::StrucMessage2& out_data, typename std::enable_if<std::is_class<app::StrucMessage2>::value>::type*);

    /* app::StrucMessage3 */
    template void BinarySerialization::Calculate<app::StrucMessage3>(const app::StrucMessage3& in_data, size_t& out_size, typename std::enable_if<std::is_class<app::StrucMessage3>::value>::type*);
    template void BinarySerialization::Serialize<app::StrucMessage3>(const app::StrucMessage3& in_data, size_t& offset, serialization::Archive& out_archive, typename std::enable_if<std::is_class<app::StrucMessage3>::value>::type*);
    template void BinarySerialization::Deserialize<app::StrucMessage3>(const serialization::Archive& in_archive, size_t& offset, app::StrucMessage3& out_data, typename std::enable_if<std::is_class<app::StrucMessage3>::value>::type*);


    /* テキスト形式シリアライズ明示的インスタンス化 */
    /* app::StrucMessage1 */
    template void TextSerialization::Serialize<app::StrucMessage1>(const app::StrucMessage1& in_data, Node& out_node, typename std::enable_if<std::is_class<app::StrucMessage1>::value>::type*);
    template void TextSerialization::Deserialize<app::StrucMessage1>(const Node& in_node, app::StrucMessage1& out_data, typename std::enable_if<std::is_class<app::StrucMessage1>::value>::type*);

    /* app::StrucMessage2 */
    template void TextSerialization::Serialize<app::StrucMessage2>(const app::StrucMessage2& in_data, Node& out_node, typename std::enable_if<std::is_class<app::StrucMessage2>::value>::type*);
    template void TextSerialization::Deserialize<app::StrucMessage2>(const Node& in_node, app::StrucMessage2& out_data, typename std::enable_if<std::is_class<app::StrucMessage2>::value>::type*);

    /* app::StrucMessage3 */
    template void TextSerialization::Serialize<app::StrucMessage3>(const app::StrucMessage3& in_data, Node& out_node, typename std::enable_if<std::is_class<app::StrucMessage3>::value>::type*);
    template void TextSerialization::Deserialize<app::StrucMessage3>(const Node& in_node, app::StrucMessage3& out_data, typename std::enable_if<std::is_class<app::StrucMessage3>::value>::type*);
}

使用例

定義したユーザデータ型に対するシリアライズ/デシリアライズを行うサンプルコードを示します。

モード切替えコンパイルスイッチ

バイナリ形式のシリアライズとテキスト形式のシリアライズを切り替えるためのコンパイルスイッチマクロを定義しています。

/* シリアライズモード:バイナリ形式 */
#define SERIALIZE_MODE_BINARY   (0)
/* シリアライズモード:テキスト形式 */
#define SERIALIZE_MODE_TEXT     (1)

/* シリアライズモード切替え */
#define SERIALIZE_MODE          (SERIALIZE_MODE_BINARY)
シリアライズテスト関数

シリアライズのテストを行う共通関数を定義します。
正しくシリアライズ/デシリアライズ出来ているかを確認するために、入力データを乱数で生成し、入力データおよび、シリアライズ⇒デシリアライズ後の出力データの内容をダンプして比較できるようにしています。

乱数でのデータ生成およびデータのダンプ処理も、今回紹介したSwallowイディオムおよびIndex Tupleイディオムを用いて実装しています。
詳細はGitHubのコードを参照してください。
機会があれば別途紹介したいと思います。

/* シリアライズテスト */
template <typename T>
void SerializeTest(const std::string& type_name)
{
    /* ログダンプ用文字列ストリーム */
    std::stringstream ss;

    /* 入力データ */
    T in_data{};

    ss << "Serialize Test : " << type_name << std::endl;

    /* 入力データを乱数で生成 */
    cpp_lib::random::RandomDataGenerator::Generate(in_data);

    ss << "-----Before Dump----" << std::endl;
    /* 入力データをダンプ */
    ss << cpp_lib::dump::DataDumper::ToString(type_name, in_data);
    ss << "--------------------" << std::endl;

#if SERIALIZE_MODE == SERIALIZE_MODE_BINARY
    /* バイナリ形式用シリアライザインスタンス生成 */
    cpp_lib::serialization::Serializer<T>& serializer = cpp_lib::serialization::SerializerFactory<T>::CreateBinarySerializer();
#elif SERIALIZE_MODE == SERIALIZE_MODE_TEXT
    /* テキスト形式用シリアライザインスタンス生成 */
    cpp_lib::serialization::Serializer<T>& serializer = cpp_lib::serialization::SerializerFactory<T>::CreateTextSerializer();
#else
#error Invalid Serialize Mode : SERIALIZE_MODE
#endif

    /* アーカイブ */
    cpp_lib::serialization::Archive archive;

    /* シリアライズ */
    serializer.Serialize(in_data, archive);

    /* シリアライズ後のデータサイズ出力 */
    ss << std::endl;
    ss << "Serialized Size : " << archive.GetSize() << std::endl;

    /* テキスト形式の場合はテキストデータをダンプ */
#if SERIALIZE_MODE == SERIALIZE_MODE_TEXT
    ss << "------XML Dump------" << std::endl;
    ss << archive.GetDataPtr();
    ss << "--------------------" << std::endl;
#endif

    ss << std::endl;

    /* 出力データ */
    T out_data{};

    /* アーカイブをデシリアライズ */
    serializer.Deserialize(archive, out_data);

    ss << "-----After Dump----" << std::endl;
    /* 出力データをダンプ */
    ss << cpp_lib::dump::DataDumper::ToString(type_name, out_data);
    ss << "--------------------" << std::endl;;

    /* 文字列ストリームをコンソールに出力 */
    std::cout << ss.str() << std::endl;
    std::cout << std::endl;
    std::cout << std::endl;
}
メイン関数

メイン関数で定義した各ユーザデータ型に対するシリアライズテスト関数を呼び出します。

int main()
{
    /* 各ユーザ定義型に対するシリアライズテスト実行 */
    SerializeTest<app::BoolMessage>("BoolMessage");
    SerializeTest<app::Int8Message>("Int8Message");
    SerializeTest<app::Int16Message>("Int16Message");
    SerializeTest<app::Int32Message>("Int32Message");
    SerializeTest<app::Int64Message>("Int64Message");
    SerializeTest<app::UInt8Message>("UInt8Message");
    SerializeTest<app::UInt16Message>("UInt16Message");
    SerializeTest<app::UInt32Message>("UInt32Message");
    SerializeTest<app::UInt64Message>("UInt64Message");
    SerializeTest<app::Float32Message>("Float32Message");
    SerializeTest<app::Float64Message>("Float64Message");
    SerializeTest<app::StringMessage>("StringMessage");
    SerializeTest<app::ArrayMessage>("ArrayMessage");
    SerializeTest<app::VectorMessage>("VectorMessage");
    SerializeTest<app::MapMessage>("MapMessage");
    SerializeTest<app::EnumMessage1>("EnumMessage1");
    SerializeTest<app::EnumMessage2>("EnumMessage2");
    SerializeTest<app::StrucMessage1>("StrucMessage1");
    SerializeTest<app::StrucMessage2>("StrucMessage2");
    SerializeTest<app::StrucMessage3>("StrucMessage3");

    return 0;
}

実行結果

バイナリ形式、テキスト形式それぞれの実行結果を示します。
乱数でデータを生成しているため、実行するたびに異なる結果となります。

バイナリ形式

Serialize Test : BoolMessage
-----Before Dump----
- BoolMessage : true
--------------------

Serialized Size : 1

-----After Dump----
- BoolMessage : true
--------------------



Serialize Test : Int8Message
-----Before Dump----
- Int8Message : -97
--------------------

Serialized Size : 1

-----After Dump----
- Int8Message : -97
--------------------



Serialize Test : Int16Message
-----Before Dump----
- Int16Message : 557
--------------------

Serialized Size : 2

-----After Dump----
- Int16Message : 557
--------------------



Serialize Test : Int32Message
-----Before Dump----
- Int32Message : -470
--------------------

Serialized Size : 4

-----After Dump----
- Int32Message : -470
--------------------



Serialize Test : Int64Message
-----Before Dump----
- Int64Message : -113
--------------------

Serialized Size : 8

-----After Dump----
- Int64Message : -113
--------------------



Serialize Test : UInt8Message
-----Before Dump----
- UInt8Message : 85
--------------------

Serialized Size : 1

-----After Dump----
- UInt8Message : 85
--------------------



Serialize Test : UInt16Message
-----Before Dump----
- UInt16Message : 247
--------------------

Serialized Size : 2

-----After Dump----
- UInt16Message : 247
--------------------



Serialize Test : UInt32Message
-----Before Dump----
- UInt32Message : 1981
--------------------

Serialized Size : 4

-----After Dump----
- UInt32Message : 1981
--------------------



Serialize Test : UInt64Message
-----Before Dump----
- UInt64Message : 1620
--------------------

Serialized Size : 8

-----After Dump----
- UInt64Message : 1620
--------------------



Serialize Test : Float32Message
-----Before Dump----
- Float32Message : 0.276794
--------------------

Serialized Size : 4

-----After Dump----
- Float32Message : 0.276794
--------------------



Serialize Test : Float64Message
-----Before Dump----
- Float64Message : 921.443368
--------------------

Serialized Size : 8

-----After Dump----
- Float64Message : 921.443368
--------------------



Serialize Test : StringMessage
-----Before Dump----
- StringMessage : NsPtoRr6l
--------------------

Serialized Size : 18

-----After Dump----
- StringMessage : NsPtoRr6l
--------------------



Serialize Test : ArrayMessage
-----Before Dump----
- ArrayMessage
  -377, 887, -930, 571
--------------------

Serialized Size : 24

-----After Dump----
- ArrayMessage
  -377, 887, -930, 571
--------------------



Serialize Test : VectorMessage
-----Before Dump----
- VectorMessage
  886.725027, -888.930458, 653.026661, 600.011359, 17.384446, 923.696587, -418.051060, 560.708495, -860.235959
--------------------

Serialized Size : 80

-----After Dump----
- VectorMessage
  886.725027, -888.930458, 653.026661, 600.011359, 17.384446, 923.696587, -418.051060, 560.708495, -860.235959
--------------------



Serialize Test : MapMessage
-----Before Dump----
- MapMessage
  - [0]
    - first : -972
    - second : -76.153916
  - [1]
    - first : -786
    - second : -671.554764
  - [2]
    - first : -722
    - second : -101.852106
  - [3]
    - first : -698
    - second : -818.168871
  - [4]
    - first : -415
    - second : 912.755329
  - [5]
    - first : -242
    - second : -645.595839
  - [6]
    - first : 39
    - second : -959.489234
  - [7]
    - first : 369
    - second : -821.034684
  - [8]
    - first : 428
    - second : 79.652055
  - [9]
    - first : 597
    - second : 808.364866
  - [10]
    - first : 876
    - second : 794.861590
--------------------

Serialized Size : 140

-----After Dump----
- MapMessage
  - [0]
    - first : -972
    - second : -76.153916
  - [1]
    - first : -786
    - second : -671.554764
  - [2]
    - first : -722
    - second : -101.852106
  - [3]
    - first : -698
    - second : -818.168871
  - [4]
    - first : -415
    - second : 912.755329
  - [5]
    - first : -242
    - second : -645.595839
  - [6]
    - first : 39
    - second : -959.489234
  - [7]
    - first : 369
    - second : -821.034684
  - [8]
    - first : 428
    - second : 79.652055
  - [9]
    - first : 597
    - second : 808.364866
  - [10]
    - first : 876
    - second : 794.861590
--------------------



Serialize Test : EnumMessage1
-----Before Dump----
- EnumMessage1 : 1
--------------------

Serialized Size : 4

-----After Dump----
- EnumMessage1 : 1
--------------------



Serialize Test : EnumMessage2
-----Before Dump----
- EnumMessage2 : 2
--------------------

Serialized Size : 4

-----After Dump----
- EnumMessage2 : 2
--------------------



Serialize Test : StrucMessage1
-----Before Dump----
- StrucMessage1
  - bool_value : true
  - uint_value : 345
  - float_value : -412.287720
--------------------

Serialized Size : 9

-----After Dump----
- StrucMessage1
  - bool_value : true
  - uint_value : 345
  - float_value : -412.287720
--------------------



Serialize Test : StrucMessage2
-----Before Dump----
- StrucMessage2
  - float_array
    -945.414795, 771.739014, 445.108154, -849.261414, 124.613403, 327.225830, 341.188965, 99.228516
  - ulong_vec
    967, 415, 1863, 1580, 864, 1263, 1044, 173, 1742, 63, 1885, 247, 724, 1333, 378, 1391, 424, 1067, 617, 1894, 1574, 926, 1232, 1594, 1023, 746, 1857, 22, 1394, 1710
--------------------

Serialized Size : 288

-----After Dump----
- StrucMessage2
  - float_array
    -945.414795, 771.739014, 445.108154, -849.261414, 124.613403, 327.225830, 341.188965, 99.228516
  - ulong_vec
    967, 415, 1863, 1580, 864, 1263, 1044, 173, 1742, 63, 1885, 247, 724, 1333, 378, 1391, 424, 1067, 617, 1894, 1574, 926, 1232, 1594, 1023, 746, 1857, 22, 1394, 1710
--------------------



Serialize Test : StrucMessage3
-----Before Dump----
- StrucMessage3
  - string_value : neb5hpDH
  - enum_str_map
    - [0]
      - first : 1
      - second : tPhNWivV6ywsRz8
--------------------

Serialized Size : 53

-----After Dump----
- StrucMessage3
  - string_value : neb5hpDH
  - enum_str_map
    - [0]
      - first : 1
      - second : tPhNWivV6ywsRz8
--------------------

テキスト形式

Serialize Test : BoolMessage
-----Before Dump----
- BoolMessage : true
--------------------

Serialized Size : 24
------XML Dump------
{
    "root": "true"
}
--------------------

-----After Dump----
- BoolMessage : true
--------------------



Serialize Test : Int8Message
-----Before Dump----
- Int8Message : -66
--------------------

Serialized Size : 23
------XML Dump------
{
    "root": "-66"
}
--------------------

-----After Dump----
- Int8Message : -66
--------------------



Serialize Test : Int16Message
-----Before Dump----
- Int16Message : -660
--------------------

Serialized Size : 24
------XML Dump------
{
    "root": "-660"
}
--------------------

-----After Dump----
- Int16Message : -660
--------------------



Serialize Test : Int32Message
-----Before Dump----
- Int32Message : -450
--------------------

Serialized Size : 24
------XML Dump------
{
    "root": "-450"
}
--------------------

-----After Dump----
- Int32Message : -450
--------------------



Serialize Test : Int64Message
-----Before Dump----
- Int64Message : 968
--------------------

Serialized Size : 23
------XML Dump------
{
    "root": "968"
}
--------------------

-----After Dump----
- Int64Message : 968
--------------------



Serialize Test : UInt8Message
-----Before Dump----
- UInt8Message : 130
--------------------

Serialized Size : 23
------XML Dump------
{
    "root": "130"
}
--------------------

-----After Dump----
- UInt8Message : 130
--------------------



Serialize Test : UInt16Message
-----Before Dump----
- UInt16Message : 1021
--------------------

Serialized Size : 24
------XML Dump------
{
    "root": "1021"
}
--------------------

-----After Dump----
- UInt16Message : 1021
--------------------



Serialize Test : UInt32Message
-----Before Dump----
- UInt32Message : 451
--------------------

Serialized Size : 23
------XML Dump------
{
    "root": "451"
}
--------------------

-----After Dump----
- UInt32Message : 451
--------------------



Serialize Test : UInt64Message
-----Before Dump----
- UInt64Message : 569
--------------------

Serialized Size : 23
------XML Dump------
{
    "root": "569"
}
--------------------

-----After Dump----
- UInt64Message : 569
--------------------



Serialize Test : Float32Message
-----Before Dump----
- Float32Message : 131.467041
--------------------

Serialized Size : 30
------XML Dump------
{
    "root": "0x43037790"
}
--------------------

-----After Dump----
- Float32Message : 131.467041
--------------------



Serialize Test : Float64Message
-----Before Dump----
- Float64Message : 676.831881
--------------------

Serialized Size : 38
------XML Dump------
{
    "root": "0x408526a7b15eee50"
}
--------------------

-----After Dump----
- Float64Message : 676.831881
--------------------



Serialize Test : StringMessage
-----Before Dump----
- StringMessage : r4LhVs
--------------------

Serialized Size : 26
------XML Dump------
{
    "root": "r4LhVs"
}
--------------------

-----After Dump----
- StringMessage : r4LhVs
--------------------



Serialize Test : ArrayMessage
-----Before Dump----
- ArrayMessage
  711, 671, 775, -248
--------------------

Serialized Size : 117
------XML Dump------
{
    "root": {
        "item": "711",
        "item": "671",
        "item": "775",
        "item": "-248"
    }
}
--------------------

-----After Dump----
- ArrayMessage
  711, 671, 775, -248
--------------------



Serialize Test : VectorMessage
-----Before Dump----
- VectorMessage
  -918.270491, 567.724274, -829.875955, -347.300835
--------------------

Serialized Size : 176
------XML Dump------
{
    "root": {
        "item": "0xc08cb229f73fc5ba",
        "item": "0x4081bdcb5028a146",
        "item": "0xc089ef01f46f728d",
        "item": "0xc075b4d038311c5c"
    }
}
--------------------

-----After Dump----
- VectorMessage
  -918.270491, 567.724274, -829.875955, -347.300835
--------------------



Serialize Test : MapMessage
-----Before Dump----
- MapMessage
  - [0]
    - first : -572
    - second : 534.903451
  - [1]
    - first : -454
    - second : 717.369277
  - [2]
    - first : -344
    - second : -404.941074
  - [3]
    - first : -207
    - second : -824.382721
  - [4]
    - first : -170
    - second : 266.270653
  - [5]
    - first : 64
    - second : -5.214582
  - [6]
    - first : 178
    - second : 705.179553
  - [7]
    - first : 233
    - second : -208.877004
  - [8]
    - first : 545
    - second : 897.333639
  - [9]
    - first : 656
    - second : 875.125018
--------------------

Serialized Size : 1028
------XML Dump------
{
    "root": {
        "item": {
            "first": "-572",
            "second": "0x4080b73a445b0786"
        },
        "item": {
            "first": "-454",
            "second": "0x40866af4476c006c"
        },
        "item": {
            "first": "-344",
            "second": "0xc0794f0ea393ca94"
        },
        "item": {
            "first": "-207",
            "second": "0xc089c30fcff65380"
        },
        "item": {
            "first": "-170",
            "second": "0x4070a454981c5d8c"
        },
        "item": {
            "first": "64",
            "second": "0xc014dbbb4f84c080"
        },
        "item": {
            "first": "178",
            "second": "0x4086096fb9bb75ee"
        },
        "item": {
            "first": "233",
            "second": "0xc06a1c1069c4df70"
        },
        "item": {
            "first": "545",
            "second": "0x408c0aab4aab4776"
        },
        "item": {
            "first": "656",
            "second": "0x408b590009a903e0"
        }
    }
}
--------------------

-----After Dump----
- MapMessage
  - [0]
    - first : -572
    - second : 534.903451
  - [1]
    - first : -454
    - second : 717.369277
  - [2]
    - first : -344
    - second : -404.941074
  - [3]
    - first : -207
    - second : -824.382721
  - [4]
    - first : -170
    - second : 266.270653
  - [5]
    - first : 64
    - second : -5.214582
  - [6]
    - first : 178
    - second : 705.179553
  - [7]
    - first : 233
    - second : -208.877004
  - [8]
    - first : 545
    - second : 897.333639
  - [9]
    - first : 656
    - second : 875.125018
--------------------



Serialize Test : EnumMessage1
-----Before Dump----
- EnumMessage1 : 1
--------------------

Serialized Size : 21
------XML Dump------
{
    "root": "1"
}
--------------------

-----After Dump----
- EnumMessage1 : 1
--------------------



Serialize Test : EnumMessage2
-----Before Dump----
- EnumMessage2 : 3
--------------------

Serialized Size : 21
------XML Dump------
{
    "root": "3"
}
--------------------

-----After Dump----
- EnumMessage2 : 3
--------------------



Serialize Test : StrucMessage1
-----Before Dump----
- StrucMessage1
  - bool_value : true
  - uint_value : 1306
  - float_value : -944.752625
--------------------

Serialized Size : 121
------XML Dump------
{
    "root": {
        "bool_value": "true",
        "uint_value": "1306",
        "float_value": "0xc46c302b"
    }
}
--------------------

-----After Dump----
- StrucMessage1
  - bool_value : true
  - uint_value : 1306
  - float_value : -944.752625
--------------------



Serialize Test : StrucMessage2
-----Before Dump----
- StrucMessage2
  - float_array
    737.617188, 87.140381, -413.917358, 613.480347, 576.777954, 985.471558, -609.963989, -687.853149
  - ulong_vec
    1334, 1720, 1684, 935, 1735, 1085, 314, 977, 403, 1725, 392, 517, 1157
--------------------

Serialized Size : 722
------XML Dump------
{
    "root": {
        "float_array": {
            "item": "0x44386780",
            "item": "0x42ae47e0",
            "item": "0xc3cef56c",
            "item": "0x44195ebe",
            "item": "0x441031ca",
            "item": "0x44765e2e",
            "item": "0xc4187db2",
            "item": "0xc42bf69a"
        },
        "ulong_vec": {
            "item": "1334",
            "item": "1720",
            "item": "1684",
            "item": "935",
            "item": "1735",
            "item": "1085",
            "item": "314",
            "item": "977",
            "item": "403",
            "item": "1725",
            "item": "392",
            "item": "517",
            "item": "1157"
        }
    }
}
--------------------

-----After Dump----
- StrucMessage2
  - float_array
    737.617188, 87.140381, -413.917358, 613.480347, 576.777954, 985.471558, -609.963989, -687.853149
  - ulong_vec
    1334, 1720, 1684, 935, 1735, 1085, 314, 977, 403, 1725, 392, 517, 1157
--------------------



Serialize Test : StrucMessage3
-----Before Dump----
- StrucMessage3
  - string_value : 5Gx0tVhsKrhvw
  - enum_str_map
    - [0]
      - first : 1
      - second : oB
    - [1]
      - first : 2
      - second : kr7k2
--------------------

Serialized Size : 300
------XML Dump------
{
    "root": {
        "string_value": "5Gx0tVhsKrhvw",
        "enum_str_map": {
            "item": {
                "first": "1",
                "second": "oB"
            },
            "item": {
                "first": "2",
                "second": "kr7k2"
            }
        }
    }
}
--------------------

-----After Dump----
- StrucMessage3
  - string_value : 5Gx0tVhsKrhvw
  - enum_str_map
    - [0]
      - first : 1
      - second : oB
    - [1]
      - first : 2
      - second : kr7k2
--------------------

あとがき

今回は任意のデータ型(クラス/構造体)に対してシリアライズできるようにする方法について説明しました。
次回はおまけとして、SwallowイディオムおよびIndex Tupleイディオムを用いた、乱数でのデータ生成およびデータのダンプ処理について説明したいと思います。

C++で任意のデータ型をシリアライズ - その3 テキスト形式シリアライズ編

まえがき

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

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

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


前提条件

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

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


実装

テキスト形式シリアライズ

テキスト形式シリアライズクラスでは、任意のデータ型をテキスト形式(XML or JSON)に変換して、変換されたテキスト(文字列)をバイト配列としてアーカイブに書き込む処理を行います。

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


ライブラリインクルード

テキスト形式へのシリアライズには、boostライブラリのproperty_tree機能を使用します。
そのため、下記のヘッダをインクルードします。
property_treeはヘッダオンリーのライブラリのため、ヘッダインクルードのみで使用可能です。

#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/property_tree/json_parser.hpp>


マクロ

シリアライズの形式をXMLJSONで切り替えるためのマクロを定義します。

#define TEXT_SERIALIZE_MODE_XML     (0)
#define TEXT_SERIALIZE_MODE_JSON    (1)

#define TEXT_SERIALIZE_MODE         (TEXT_SERIALIZE_MODE_XML)


ノード

property_treeの木構造を表現するためのboost::property_tree::ptreeを、分かりやすいようにNodeという名前でエイリアス定義します。

/* ツリーノード型 */
using Node = boost::property_tree::ptree;


コンストラク

シリアライズクラスは複数インスタンスを生成する必要がないため、シングルトンとして実装しています。
メンバ変数などはないため、コンストラクタはデフォルト定義しています。

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

private:
    /* コンストラクタ */
    TextSerialization() = default;

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

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


インタフェース

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

/* 入力データをシリアライズしてアーカイブに変換 */
template <typename T>
void Serialize(const T& in_data, const string_t& name, Archive& out_archive)
{
    /* ルートノード */
    Node root_node{};

    /* シリアライズ */
    this->Serialize(in_data, name, root_node);

    /* シリアライズしたツリーノードをテキストに変換し、アーカイブにセット */
    this->WriteText(root_node, out_archive);
}

/* アーカイブをデシリアライズして出力データに変換 */
template <typename T>
void Deserialize(const Archive& in_archive, const string_t& name, T& out_data)
{
    /* ルートノード */
    Node root_node{};

    /* アーカイブをテキストに変換し、ツリーノードに変換 */
    this->ReadText(in_archive, root_node);

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

Serialize()ではNode型のルートノードを定義し、指定されたデータ型に応じたシリアライズ(ツリーノードに要素を追加)を行うSerialize()を呼び出します。構造体型などの場合は各メンバに対してSerialize()を呼び出し、すべてのデータをシリアライズします。
その後、シリアライズしたツリーノードをテキスト形式(文字列)に変換し、アーカイブにセットします。

Deserialize()では、アーカイブからテキストデータ(文字列)を取得し、指定形式(XML/JSON)でパース処理を行いツリーノードに変換します。 その後、指定されたデータ型に応じたDeserialize()を呼び出してデシリアライズ(ツリーノードから要素を取得し、元のデータに復元)します。構造体型などの場合は各メンバに対してDeserialize()を呼び出し、すべてのメンバをデシリアライズします。


内部関数

プライベートメンバ関数としては、データ型に応じたSerialize()Deserialize()やテキスト ⇔ アーカイブ変換処理を実装します。
データ型に応じた処理内容を詳しく説明していきます。


論理型・整数型

論理型・整数型では、そのままツリーノードに変換が可能なため、ツリーノードへの追加・取得を行う処理を実装します。

/* 整数型シリアライズ */
template<typename T, type_traits::concept_t<std::is_integral<T>::value> = nullptr>
void Serialize(const T& in_data, Node& out_node)
{
    /* ツリーノードに値を追加 */
    out_node.put_value(in_data);
}

/* 整数型デシリアライズ */
template<typename T, type_traits::concept_t<std::is_integral<T>::value> = nullptr>
void Deserialize(const Node& in_node, T& out_data)
{
    /* ツリーノードから要素取得 */
    if (auto value = in_node.get_value_optional<T>())
    {
        /* ツリーノード要素から値を取得 */
        out_data = value.get();
    }
    /* 要素取得失敗時は例外創出 */
    else
    {
        THROW_APP_EXCEPTION("Value is nothing");
    }
}

Serialize()ではput_value()でツリーノードにそのままデータを追加します。
Deserialize()では、get_value_optional<T>()でツリーノードから値の取得を試み、取得に成功したら値を取り出します。
値の取得に失敗した場合は異常な状態となっているため例外を送出します。


浮動小数

浮動小数型の場合は、そのままシリアライズしてテキストに変換すると、一定の桁数で丸められてしまい、デシリアライズで復元した際に、完全に同じ値にならないケースがあります。
そのため、浮動小数型を16進数の文字列に変換してシリアライズ(テキストに変換)し、デシリアライズ時には16進数文字列から元の浮動小数型に復元するようにします。

/* 浮動小数型シリアライズ */
template<typename T, type_traits::concept_t<std::is_floating_point<T>::value> = nullptr>
void Serialize(const T& in_data, Node& out_node)
{
    /* 16進数文字列 */
    string_t in_str{};

    /* 浮動小数データを16進数文字列に変換 */
    this->ToHexString(in_data, in_str);

    /* ツリーノードに16進数文字列に変換した値を追加 */
    out_node.put_value(in_str);
}

/* 浮動小数型デシリアライズ */
template<typename T, type_traits::concept_t<std::is_floating_point<T>::value> = nullptr>
void Deserialize(const Node& in_node, T& out_data)
{
    /* 16進数文字列 */
    string_t out_str{};

    /* ツリーノードから要素取得 */
    if (auto value = in_node.get_value_optional<string_t>())
    {
        /* ツリーノード要素から値を取得 */
        out_str = value.get();

        /* 取得した値(16進数文字列)を浮動小数データに変換 */
        this->FromHexString(out_str, out_data);
    }
    /* 要素取得失敗時は例外創出 */
    else
    {
        THROW_APP_EXCEPTION("Value is nothing");
    }
}

Serialize()ではToHexString()で16進数文字列に変換し、put_value()でツリーノードにデータを追加します。
Deserialize()では、get_value_optional<T>()でツリーノードから文字列(string)として値の取得を試み、取得に成功したら値を取り出し、FromHexString()で元の浮動小数型に復元しています。
値の取得に失敗した場合は異常な状態となっているため例外を送出します。

/* 浮動小数データ(32bit)を16進数文字列に変換 */
inline void ToHexString(const float32_t& in_data, string_t& out_data)
{
    /* 文字列ストリーム */
    std::stringstream ss;

    /* 浮動小数 <=> 整数変換器 */
    binary::FloatIntConverter<float32_t, uint32_t> converter;

    /* 浮動小数 => 整数変換 */
    uint32_t int_data = converter.ConvertToInt(in_data);

    /* 変換した整数を16進数文字列として文字列ストリームに追加 */
    ss << std::showbase;
    ss << std::setw(4);
    ss << std::hex << int_data;

    /* 文字列に変換 */
    out_data = ss.str();
}

/* 浮動小数データ(64bit)を16進数文字列に変換 */
inline void ToHexString(const float64_t& in_data, string_t& out_data)
{
    /* 文字列ストリーム */
    std::stringstream ss;

    /* 浮動小数 <=> 整数変換器 */
    binary::FloatIntConverter<float64_t, uint64_t> converter;

    /* 浮動小数 => 整数変換 */
    uint64_t int_data = converter.ConvertToInt(in_data);

    /* 変換した整数を16進数文字列として文字列ストリームに追加 */
    ss << std::showbase;
    ss << std::setw(8);
    ss << std::hex << int_data;

    /* 文字列に変換 */
    out_data = ss.str();
}

/* 16進数文字列を浮動小数(32bit)データに変換 */
inline void FromHexString(const string_t& in_data, float32_t& out_data)
{
    /* 16進数文字列を整数に変換 */
    uint32_t int_data = std::stoul(in_data, nullptr, 16);

    /* 浮動小数 <=> 整数変換器 */
    binary::FloatIntConverter<float32_t, uint32_t> converter;

    /* 整数 => 浮動小数変換 */
    out_data = converter.ConvertToFloat(int_data);
}

/* 16進数文字列を浮動小数(64bit)データに変換 */
inline void FromHexString(const string_t& in_data, float64_t& out_data)
{
    /* 16進数文字列を整数に変換 */
    uint64_t int_data = std::stoull(in_data, nullptr, 16);

    /* 浮動小数 <=> 整数変換器 */
    binary::FloatIntConverter<float64_t, uint64_t> converter;

    /* 整数 => 浮動小数変換 */
    out_data = converter.ConvertToFloat(int_data);
}

ToHexString()では、binary::FloatIntConverterConvertToInt()で一旦元の浮動小数型と同じビットサイズの符号無整数型に変換し、std::stringstreamで16進数の文字列に変換しています。
FromHexString()では、16進数文字列をstd::stoul()std::stoull()で元の浮動小数型と同じビットサイズの符号無整数型に変換し、binary::FloatIntConverterConvertToFloat()で元の浮動小数型に復元しています。
binary::FloatIntConverterの詳細については、GitHubのソースをご参照ください。


列挙型

列挙型(特にenum class)では、ツリーノードに変換できないため、基底型に変換して処理を行うようにしています。

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

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

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

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

Serialize()では、type_traits::underlying_castを用いて基底型(デフォルトではint)に変換して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 Serialize(const std::array<T, N>& in_data, const string_t& name, Node& out_node)
{
    /* 名前を指定して子ノードを追加 */
    auto& child_node = out_node.add(name, "");

    /* 全要素を走査 */
    for (const auto& item : in_data)
    {
        /* 各要素をシリアライズして子ノードに追加 */
        this->Serialize(item, "item", child_node);
    }
}

/* std::array型デシリアライズ */
template <typename T, size_t N>
void Deserialize(const Node& in_node, const string_t& name, std::array<T, N>& out_data)
{
    /* 名前を指定して子ノードを取得 */
    auto& child_node = in_node.get_child(name);

    /* インデックス */
    size_t i = 0;

    /* 子ノードの全要素を走査 */
    for (const auto& item_node_pair : child_node)
    {
        /* 要素 */
        T item{};

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

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

        /* インデックスインクリメント */
        i++;
    }
}


/* std::vector型シリアライズ */
template <typename T>
void Serialize(const std::vector<T>& in_data, const string_t& name, Node& out_node)
{
    /* 名前を指定して子ノードを追加 */
    auto& child_node = out_node.add(name, "");

    /* 全要素を走査 */
    for (const auto& item : in_data)
    {
        /* 各要素をシリアライズして子ノードに追加 */
        this->Serialize(item, "item", child_node);
    }
}

/* std::vector型デシリアライズ */
template <typename T>
void Deserialize(const Node& in_node, const string_t& name, std::vector<T>& out_data)
{
    /* 名前を指定して子ノードを取得 */
    auto& child_node = in_node.get_child(name);

    /* 出力データクリア */
    out_data.clear();

    /* 子ノードの要素数分メモリを事前確保 */
    out_data.reserve(child_node.size());

    /* 子ノードの全要素を走査 */
    for (const auto& item_node_pair : child_node)
    {
        /* 要素 */
        T item{};

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

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


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

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

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

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

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

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

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


/* std::map型シリアライズ */
template <typename TKey, typename TValue>
void Serialize(const std::map<TKey, TValue>& in_data, const string_t& name, Node& out_node)
{
    /* 名前を指定して子ノードを追加 */
    auto& child_node = out_node.add(name, "");

    /* 全要素を走査 */
    for (const auto& item : in_data)
    {
        /* 各要素をシリアライズして子ノードに追加 */
        this->Serialize(item, "item", child_node);
    }
}

/* std::map型デシリアライズ */
template <typename TKey, typename TValue>
void Deserialize(const Node& in_node, const string_t& name, std::map<TKey, TValue>& out_data)
{
    /* 名前を指定して子ノードを取得 */
    auto& child_node = in_node.get_child(name);

    /* 子ノードの全要素を走査 */
    for (const auto& item_node_pair : child_node)
    {
        /* キー要素 */
        TKey key{};

        /* キー要素デシリアライズ */
        this->Deserialize(item_node_pair.second, "first", key);

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

        /* 値要素デシリアライズ */
        this->Deserialize(item_node_pair.second, "second", value);

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

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

Serialize()では、指定された名前(メンバ変数名)で子ノード(chile_node)を追加し、コンテナの各要素に対してシリアライズを行うSerialize()を呼び出し、子ノード(chile_node)の子供(item)としてノードを追加していきます。

Deserialize()では、指定された名前(メンバ変数名)で子ノード(chile_node)を取得し、その子ノード(chile_node)の子供(item)の要素数Deserialize()を呼び出して各要素をデシリアライズします。

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

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


文字列型

文字列型では、整数型と同様、そのままツリーノードに変換が可能なため、ツリーノードへの追加・取得を行う処理を実装します。

/* 文字列型シリアライズ */
inline void Serialize(const string_t& in_data, Node& out_node)
{
    /* ツリーノードに値を追加 */
    out_node.put_value(in_data);
}

/* 文字列型デシリアライズ */
inline void Deserialize(const Node& in_node, string_t& out_data)
{
    /* ツリーノードから要素取得 */
    if (auto value = in_node.get_value_optional<string_t>())
    {
        /* ツリーノード要素から値を取得 */
        out_data = value.get();
    }
    /* 要素取得失敗時は例外創出 */
    else
    {
        THROW_APP_EXCEPTION("Value is nothing");
    }
}


クラス/構造体型

クラス/構造体型では、バイナリ形式の場合と同様、メンバ変数をタプル(std::tuple)型で列挙して各メンバに対してSerialize()Deserialize()を呼び出しています。

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

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

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

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

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

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


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

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

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

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

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

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


名前付きオブジェクト

メンバ変数やコンテナ要素など同じ階層に複数のデータが含まれている場合には、名前を付けてどのデータかを特定できるようにする必要があります。
そういった名前付きオブジェクトのシリアライズ/デシリアライズを共通化しています。

/* 名前付きオブジェクトシリアライズ */
template <typename T>
void Serialize(const T& in_data, const string_t& name, Node& out_node)
{
    /* ツリーノードに名前を指定して子ノード追加 */
    auto& child_node = out_node.add(name, "");

    /* 入力データをシリアライズして子ノードに追加 */
    this->Serialize(in_data, child_node);
}

/* 名前付きオブジェクトデシリアライズ */
template <typename T>
void Deserialize(const Node& in_node, const string_t& name, T& out_data)
{
    /* ツリーノードに名前を指定して子ノード取得 */
    auto& child_node = in_node.get_child(name);

    /* 子ノードからデータをデシリアライズ */
    this->Deserialize(child_node, out_data);
}

Serialize()では、指定された名前(メンバ変数名など)で子ノード(chile_node)を追加し、子ノードに対してSerialize()を呼び出し入力データをノードに追加します。

Deserialize()では、指定された名前(メンバ変数名など)で子ノード(chile_node)を取得し、子ノードを指定してDeserialize()を呼び出して、データを復元します。


ツリーノード ⇔ アーカイブ変換

ツリーノードとアーカイブを相互変換する関数を用意しています。
この時に使用するboostライブラリの関数を使い分けることで、XML形式とJSON形式を切り替えることができます。

/* シリアライズしたツリーノードをテキストに変換し、アーカイブにセット */
inline void WriteText(const Node& in_root_node, Archive& out_archive)
{
    /* 文字列ストリーム */
    std::stringstream ss;

#if TEXT_SERIALIZE_MODE == TEXT_SERIALIZE_MODE_XML
    /* インデントサイズ */
    const int indent = 2;

    /* XML書き込み設定(インデント文字、インデントサイズ、エンコーディング) */
    auto setting = boost::property_tree::xml_parser::xml_writer_make_settings<string_t>(' ', indent, boost::property_tree::xml_parser::widen<string_t>("utf-8"));

    /* XMLとして文字列ストリームに書き込み */
    boost::property_tree::write_xml(ss, in_root_node, setting);
#elif TEXT_SERIALIZE_MODE == TEXT_SERIALIZE_MODE_JSON
    /* JSONとして文字列ストリームに書き込み */
    boost::property_tree::write_json(ss, in_root_node);
#else
#error Invalid Serialize Mode TEXT_SERIALIZE_MODE
#endif

    /* 文字列に変換 */
    string_t in_str = ss.str();

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

    /* バッファサイズ算出(文字列長 + 終端文字(null文字)サイズ) */
    size_t buffer_size = text_len + 1;

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

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

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

/* アーカイブをテキストに変換し、ツリーノードに変換 */
inline void ReadText(const Archive& in_archive, Node& out_root_node)
{
    /* 文字列ストリーム */
    std::stringstream ss;

    /* アーカイブのデータバッファ(ポインタ)を文字列ストリームに追加 */
    ss << in_archive.GetDataPtr();

#if TEXT_SERIALIZE_MODE == TEXT_SERIALIZE_MODE_XML
    /* 文字列ストリームをXMLとしてツリーノードに読み込み */
    boost::property_tree::read_xml(ss, out_root_node);
#elif TEXT_SERIALIZE_MODE == TEXT_SERIALIZE_MODE_JSON
    /* 文字列ストリームをJSONとしてツリーノードに読み込み */
    boost::property_tree::read_json(ss, out_root_node);
#else
#error Invalid Serialize Mode TEXT_SERIALIZE_MODE
#endif
}

WriteText()では、シリアライズしたツリーノードをテキストに変換し、アーカイブにセットします。
boost::property_tree::write_xml()またはboost::property_tree::write_json()を呼び出すことでツリーノードからテキスト(XML/JSON)に変換し、文字列ストリームに追加することができます。
XMLの場合には、boost::property_tree::xml_parser::xml_writer_make_settings<string_t>()で、XMLに変換する際の設定(インデントなど)を変更できます。
その後、文字列ストリームに追加したテキストデータを文字列に変換し、文字列長 + 終端にnull文字('\0')の長さ分アーカイブのメモリを確保し、文字列データをバイト配列としてアーカイブにコピーしています。

ReadText()では、アーカイブをテキストに変換し、ツリーノードに変換します。
アーカイブのバッファデータのポインタをそのまま文字列ストリームに追加することでテキストデータを取り出すことができます。
その後、boost::property_tree::read_xml()またはboost::property_tree::read_json()を呼び出すことでテキストデータ(XML/JSON)をパースし、ツリーノードに変換することができます。


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

テキスト形式シリアライズクラス

#pragma once
#include "CommonTypes.h"
#include "TypeTraits.h"
#include "DataTypeTraits.h"
#include "Archive.h"
#include "FloatIntConverter.h"
#include "AppException.h"
#include "StringFormat.h"

#include <sstream>
#include <iomanip>

#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/property_tree/json_parser.hpp>

#define TEXT_SERIALIZE_MODE_XML     (0)
#define TEXT_SERIALIZE_MODE_JSON    (1)

#define TEXT_SERIALIZE_MODE         (TEXT_SERIALIZE_MODE_JSON)

namespace cpp_lib
{
    namespace serialization
    {
        /* テキスト形式シリアライズクラス */
        class TextSerialization
        {
        public:
            /* ツリーノード型 */
            using Node = boost::property_tree::ptree;

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

        private:
            /* コンストラクタ */
            TextSerialization() = default;

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

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

        public:
            /* 入力データをシリアライズしてアーカイブに変換 */
            template <typename T>
            void Serialize(const T& in_data, const string_t& name, serialization::Archive& out_archive)
            {
                /* ルートノード */
                Node root_node{};

                /* シリアライズ */
                this->Serialize(in_data, name, root_node);

                /* シリアライズしたツリーノードをテキストに変換し、アーカイブにセット */
                this->WriteText(root_node, out_archive);
            }

            /* アーカイブをデシリアライズして出力データに変換 */
            template <typename T>
            void Deserialize(const serialization::Archive& in_archive, const string_t& name, T& out_data)
            {
                /* ルートノード */
                Node root_node{};

                /* アーカイブをテキストに変換し、ツリーノードに変換 */
                this->ReadText(in_archive, root_node);

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

        private:
            /* 整数型シリアライズ */
            template<typename T, cpp_lib::type_traits::concept_t<std::is_integral<T>::value> = nullptr>
            void Serialize(const T& in_data, Node& out_node)
            {
                /* ツリーノードに値を追加 */
                out_node.put_value(in_data);
            }

            /* 整数型デシリアライズ */
            template<typename T, cpp_lib::type_traits::concept_t<std::is_integral<T>::value> = nullptr>
            void Deserialize(const Node& in_node, T& out_data)
            {
                /* ツリーノードから要素取得 */
                if (auto value = in_node.get_value_optional<T>())
                {
                    /* ツリーノード要素から値を取得 */
                    out_data = value.get();
                }
                /* 要素取得失敗時は例外創出 */
                else
                {
                    THROW_APP_EXCEPTION("Value is nothing");
                }
            }


            /* 浮動小数型シリアライズ */
            template<typename T, cpp_lib::type_traits::concept_t<std::is_floating_point<T>::value> = nullptr>
            void Serialize(const T& in_data, Node& out_node)
            {
                /* 16進数文字列 */
                string_t in_str{};

                /* 浮動小数データを16進数文字列に変換 */
                this->ToHexString(in_data, in_str);

                /* ツリーノードに16進数文字列に変換した値を追加 */
                out_node.put_value(in_str);
            }

            /* 浮動小数型デシリアライズ */
            template<typename T, cpp_lib::type_traits::concept_t<std::is_floating_point<T>::value> = nullptr>
            void Deserialize(const Node& in_node, T& out_data)
            {
                /* 16進数文字列 */
                string_t out_str{};

                /* ツリーノードから要素取得 */
                if (auto value = in_node.get_value_optional<string_t>())
                {
                    /* ツリーノード要素から値を取得 */
                    out_str = value.get();

                    /* 取得した値(16進数文字列)を浮動小数データに変換 */
                    this->FromHexString(out_str, out_data);
                }
                /* 要素取得失敗時は例外創出 */
                else
                {
                    THROW_APP_EXCEPTION("Value is nothing");
                }
            }

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

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

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

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

            /* std::array型シリアライズ */
            template <typename T, size_t N>
            void Serialize(const std::array<T, N>& in_data, const string_t& name, Node& out_node)
            {
                /* 名前を指定して子ノードを追加 */
                auto& child_node = out_node.add(name, "");

                /* 全要素を走査 */
                for (const auto& item : in_data)
                {
                    /* 各要素をシリアライズして子ノードに追加 */
                    this->Serialize(item, "item", child_node);
                }
            }

            /* std::array型デシリアライズ */
            template <typename T, size_t N>
            void Deserialize(const Node& in_node, const string_t& name, std::array<T, N>& out_data)
            {
                /* 名前を指定して子ノードを取得 */
                auto& child_node = in_node.get_child(name);

                /* インデックス */
                size_t i = 0;

                /* 子ノードの全要素を走査 */
                for (const auto& item_node_pair : child_node)
                {
                    /* 要素 */
                    T item{};

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

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

                    /* インデックスインクリメント */
                    i++;
                }
            }


            /* std::vector型シリアライズ */
            template <typename T>
            void Serialize(const std::vector<T>& in_data, const string_t& name, Node& out_node)
            {
                /* 名前を指定して子ノードを追加 */
                auto& child_node = out_node.add(name, "");

                /* 全要素を走査 */
                for (const auto& item : in_data)
                {
                    /* 各要素をシリアライズして子ノードに追加 */
                    this->Serialize(item, "item", child_node);
                }
            }

            /* std::vector型デシリアライズ */
            template <typename T>
            void Deserialize(const Node& in_node, const string_t& name, std::vector<T>& out_data)
            {
                /* 名前を指定して子ノードを取得 */
                auto& child_node = in_node.get_child(name);

                /* 出力データクリア */
                out_data.clear();

                /* 子ノードの要素数分メモリを事前確保 */
                out_data.reserve(child_node.size());

                /* 子ノードの全要素を走査 */
                for (const auto& item_node_pair : child_node)
                {
                    /* 要素 */
                    T item{};

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

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


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

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

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

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

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

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

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


            /* std::map型シリアライズ */
            template <typename TKey, typename TValue>
            void Serialize(const std::map<TKey, TValue>& in_data, const string_t& name, Node& out_node)
            {
                /* 名前を指定して子ノードを追加 */
                auto& child_node = out_node.add(name, "");

                /* 全要素を走査 */
                for (const auto& item : in_data)
                {
                    /* 各要素をシリアライズして子ノードに追加 */
                    this->Serialize(item, "item", child_node);
                }
            }

            /* std::map型デシリアライズ */
            template <typename TKey, typename TValue>
            void Deserialize(const Node& in_node, const string_t& name, std::map<TKey, TValue>& out_data)
            {
                /* 名前を指定して子ノードを取得 */
                auto& child_node = in_node.get_child(name);

                /* 子ノードの全要素を走査 */
                for (const auto& item_node_pair : child_node)
                {
                    /* キー要素 */
                    TKey key{};

                    /* キー要素デシリアライズ */
                    this->Deserialize(item_node_pair.second, "first", key);

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

                    /* 値要素デシリアライズ */
                    this->Deserialize(item_node_pair.second, "second", 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 SerializeTupleImple(Node& out_node, TUPLE&& t, std::index_sequence<I...>)
            {
                /* swallowイディオムでクラス/構造体(class/struct)型メンバのタプルを順次展開し、シリアライズ */
                using swallow = std::initializer_list<int>;

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

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

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

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

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


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

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

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

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


            /* 名前付きオブジェクトシリアライズ */
            template <typename T>
            void Serialize(const T& in_data, const string_t& name, Node& out_node)
            {
                /* ツリーノードに名前を指定して子ノード追加 */
                auto& child_node = out_node.add(name, "");

                /* 入力データをシリアライズして子ノードに追加 */
                this->Serialize(in_data, child_node);
            }

            /* 名前付きオブジェクトデシリアライズ */
            template <typename T>
            void Deserialize(const Node& in_node, const string_t& name, T& out_data)
            {
                /* ツリーノードに名前を指定して子ノード取得 */
                auto& child_node = in_node.get_child(name);

                /* 子ノードからデータをデシリアライズ */
                this->Deserialize(child_node, out_data);
            }

        private:
            /* 文字列型シリアライズ */
            inline void Serialize(const string_t& in_data, Node& out_node)
            {
                /* ツリーノードに値を追加 */
                out_node.put_value(in_data);
            }

            /* 文字列型デシリアライズ */
            inline void Deserialize(const Node& in_node, string_t& out_data)
            {
                /* ツリーノードから要素取得 */
                if (auto value = in_node.get_value_optional<string_t>())
                {
                    /* ツリーノード要素から値を取得 */
                    out_data = value.get();
                }
                /* 要素取得失敗時は例外創出 */
                else
                {
                    THROW_APP_EXCEPTION("Value is nothing");
                }
            }

            /* シリアライズしたツリーノードをテキストに変換し、アーカイブにセット */
            inline void WriteText(const Node& in_root_node, serialization::Archive& out_archive)
            {
                /* 文字列ストリーム */
                std::stringstream ss;

#if TEXT_SERIALIZE_MODE == TEXT_SERIALIZE_MODE_XML
                /* インデントサイズ */
                const int indent = 2;

                /* XML書き込み設定(インデント文字、インデントサイズ、エンコーディング) */
                auto setting = boost::property_tree::xml_parser::xml_writer_make_settings<string_t>(' ', indent, boost::property_tree::xml_parser::widen<string_t>("utf-8"));

                /* XMLとして文字列ストリームに書き込み */
                boost::property_tree::write_xml(ss, in_root_node, setting);
#elif TEXT_SERIALIZE_MODE == TEXT_SERIALIZE_MODE_JSON

                /* JSONとして文字列ストリームに書き込み */
                boost::property_tree::write_json(ss, in_root_node);
#else
#error Invalid Serialize Mode TEXT_SERIALIZE_MODE
#endif

                /* 文字列に変換 */
                string_t in_str = ss.str();

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

                /* バッファサイズ算出(文字列長 + 終端文字(null文字)サイズ) */
                size_t buffer_size = text_len + 1;

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

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

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

            /* アーカイブをテキストに変換し、ツリーノードに変換 */
            inline void ReadText(const serialization::Archive& in_archive, Node& out_root_node)
            {
                /* 文字列ストリーム */
                std::stringstream ss;

                /* アーカイブのデータバッファ(ポインタ)を文字列ストリームに追加 */
                ss << in_archive.GetDataPtr();

#if TEXT_SERIALIZE_MODE == TEXT_SERIALIZE_MODE_XML
                /* 文字列ストリームをXMLとしてツリーノードに読み込み */
                boost::property_tree::read_xml(ss, out_root_node);
#elif TEXT_SERIALIZE_MODE == TEXT_SERIALIZE_MODE_JSON
                /* 文字列ストリームをJSONとしてツリーノードに読み込み */
                boost::property_tree::read_json(ss, out_root_node);
#else
#error Invalid Serialize Mode TEXT_SERIALIZE_MODE
#endif
            }

            /* 浮動小数データ(32bit)を16進数文字列に変換 */
            inline void ToHexString(const float32_t& in_data, string_t& out_data)
            {
                /* 文字列ストリーム */
                std::stringstream ss;

                /* 浮動小数 <=> 整数変換器 */
                cpp_lib::binary::FloatIntConverter<float32_t, uint32_t> converter;

                /* 浮動小数 => 整数変換 */
                uint32_t int_data = converter.ConvertToInt(in_data);

                /* 変換した整数を16進数文字列として文字列ストリームに追加 */
                ss << std::showbase;
                ss << std::setw(4);
                ss << std::hex << int_data;

                /* 文字列に変換 */
                out_data = ss.str();
            }

            /* 浮動小数データ(64bit)を16進数文字列に変換 */
            inline void ToHexString(const float64_t& in_data, string_t& out_data)
            {
                /* 文字列ストリーム */
                std::stringstream ss;

                /* 浮動小数 <=> 整数変換器 */
                cpp_lib::binary::FloatIntConverter<float64_t, uint64_t> converter;

                /* 浮動小数 => 整数変換 */
                uint64_t int_data = converter.ConvertToInt(in_data);

                /* 変換した整数を16進数文字列として文字列ストリームに追加 */
                ss << std::showbase;
                ss << std::setw(8);
                ss << std::hex << int_data;

                /* 文字列に変換 */
                out_data = ss.str();
            }

            /* 16進数文字列を浮動小数(32bit)データに変換 */
            inline void FromHexString(const string_t& in_data, float32_t& out_data)
            {
                /* 16進数文字列を整数に変換 */
                uint32_t int_data = std::stoul(in_data, nullptr, 16);

                /* 浮動小数 <=> 整数変換器 */
                cpp_lib::binary::FloatIntConverter<float32_t, uint32_t> converter;

                /* 整数 => 浮動小数変換 */
                out_data = converter.ConvertToFloat(int_data);
            }

            /* 16進数文字列を浮動小数(64bit)データに変換 */
            inline void FromHexString(const string_t& in_data, float64_t& out_data)
            {
                /* 16進数文字列を整数に変換 */
                uint64_t int_data = std::stoull(in_data, nullptr, 16);

                /* 浮動小数 <=> 整数変換器 */
                cpp_lib::binary::FloatIntConverter<float64_t, uint64_t> converter;

                /* 整数 => 浮動小数変換 */
                out_data = converter.ConvertToFloat(int_data);
            }
        };
    }
}

あとがき

今回はテキスト形式でのシリアライズ処理を実装しました。
次回は、任意のデータ型で実際にシリアライズを行う方法について説明します。

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;
};

あとがき

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

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クラスのインスタンスを静的に生成し、そのインスタンスの参照を返します。

あとがき

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

C++でログ出力

まえがき

今回は、以下のような機能を持ったログ出力クラス(Logger)を実装します。

  • Infomation / Warning / Errorのログを使い分けられる
  • 各ログは分かりやすいように色付きで表示
  • 文字列フォーマッティング対応
  • マルチスレッド時の排他処理
  • 同時にファイルへのログ出力可能

なお、以前作成した文字列フォーマット関数StringFormatを使用しているので、必要に応じて参照してください。

前提条件

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

OS Ver Compiler Remarks
Windows 10 Visual Studio 2019

実装

以下の2つのクラスに分けて実装します。

  • File Logger : ファイルへのログ出力クラス
  • Logger : ログ出力インタフェースクラス

File Loggerクラスは、Loggerクラスで使用され、コンソールへのログ出力と同時にファイルへもログ出力を行います。

File Loggerクラス

ファイルへのログ出力は、標準ライブラリのfstreamクラスを使って、ファイルのオープン、クローズ、文字列出力を行う関数を実装するだけです。

【FileLogger.h】

#pragma once
#include <fstream>

/* File Loggerクラス宣言 */
class FileLogger
{
public:
    /* コンストラクタ */
    FileLogger();

    /* コンストラクタ */
    FileLogger(const std::string& file_path);

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

    /* ファイルオープン */
    void Open(const std::string& file_path);

    /* ファイルクローズ */
    void Close();

    /* ファイルオープン確認 */
    bool IsOpened();

    /* ファイル書き込み */
    void Write(const std::string& log);

private:
    /* ログファイルパス */
    std::string m_FilePath;

    /* ファイル出力ストリーム */
    std::ofstream m_Stream;
};


【FileLogger.cpp】

#include "FileLogger.h"

/* コンストラクタ */
FileLogger::FileLogger()
    : m_FilePath("")
    , m_Stream()
{
    /* Nothing to do */
}

/* コンストラクタ */
FileLogger::FileLogger(const std::string& file_path)
    : m_FilePath(file_path)
    , m_Stream()
{
    /* ファイルオープン */
    this->Open(file_path);
}

/* デストラクタ */
FileLogger::~FileLogger()
{
    /* ファイルクローズ */
    this->Close();
}

/* ファイルオープン */
void FileLogger::Open(const std::string& file_path)
{
    /* ファイルが開いていなかったら */
    if (this->IsOpened() == false)
    {
        this->m_Stream.open(file_path, std::ios::out);
    }
}

/* ファイルクローズ */
void FileLogger::Close()
{
    /* ファイルが開いていたら */
    if (this->IsOpened() == true)
    {
        this->m_Stream.close();
    }
}

/* ファイルオープン確認 */
bool FileLogger::IsOpened()
{
    return this->m_Stream.is_open();
}

/* ファイル書き込み */
void FileLogger::Write(const std::string& log)
{
    /* ファイルが開いていたら */
    if (this->IsOpened() == true)
    {
        /* ファイルストリームに書き込み */
        this->m_Stream << log << std::endl;
    }
}


Loggerクラス

ログ出力は、前述の要件を満たすために、以下のように実装します。

  1. どこから呼んでも同じインスタンスにアクセスできるようにシングルトンで実装する
  2. StringFormat関数で文字列フォーマット可能なインタフェースを定義する
  3. ログ出力時にはMutexによる排他処理を行う
  4. コンソールへのログ出力時に制御コードで文字色を変更する
  5. コンソールへのログ出力と同時にFileLoggerクラスでファイルへのログ出力も行う
1. シングルトンで実装

まずは、どこから呼んでも同じインスタンスにアクセスできるように、シングルトンでLoggerクラスを定義します。

このとき、外部からインスタンスを取得できないように、シングルトンインスタンス取得関数をprivateにしておきます。

また、シングルトンにするためにコンストラクタ/デストラクタをprivateにし、コピー/ムーブを禁止します。

【Logger.h】

#include <string>

/* Loggerクラス宣言 */
class Logger
{
private:
    /* シングルトンインスタンス取得 */
    static Logger& GetInstance();

private:
    /* コンストラクタ */
    Logger();
    /* デストラクタ */
    ~Logger();

public:
    /* コピーコンストラクタを削除 */
    Logger(const Logger&) = delete;
    /* ムーブコンストラクタを削除 */
    Logger(Logger&&) = delete;
    /* コピー代入オペレータを削除 */
    Logger& operator=(const Logger&) = delete;
    /* ムーブ代入オペレータを削除 */
    Logger& operator=(Logger&&) = delete;

private:
    /* Informationログ出力 */
    void LogInfo(const std::string& message);
    /* Warningログ出力 */
    void LogWarn(const std::string& message);
    /* Errorログ出力 */
    void LogError(const std::string& message);
}


【Lgger.cpp】

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


2. 文字列フォーマット可能なインタフェースを定義

書式付き文字列のフォーマットが可能なログ出力インタフェース関数を定義します。

インタフェース関数は、クラスメソッド(static)として定義し、Information / Warning / Errorそれぞれ用の関数を用意します。

各関数の処理としては、StringFormat関数を使って、書式付き文字列をフォーマットして、シングルトンインスタンスのInformation / Warning / Error出力関数を呼び出します。

#include "StringFormat.h"

/* Loggerクラス宣言 */
class Logger
{
public:
    /* 書式指定Informationログ */
    template<typename ... Args>
    static void Info(const std::string& format, Args&& ... args)
    {
        /* 書式フォーマットしてシングルトンインスタンスのInformationログ出力呼び出し */
        Logger::GetInstance().LogInfo(StringFormat(format, std::forward<Args>(args) ...));
    }

    /* 書式指定Warningログ */
    template<typename ... Args>
    static void Warn(const std::string& format, Args&& ... args)
    {
        /* 書式フォーマットしてシングルトンインスタンスのWarningログ出力呼び出し */
        Logger::GetInstance().LogWarn(StringFormat(format, std::forward<Args>(args) ...));
    }

    /* 書式指定Errorログ */
    template<typename ... Args>
    static void Error(const std::string& format, Args&& ... args)
    {
        /* 書式フォーマットしてシングルトンインスタンスのErrorログ出力呼び出し */
        Logger::GetInstance().LogError(StringFormat(format, std::forward<Args>(args) ...));
    }

    /* 省略 */
}


3. Mutexによる排他制御

std::coutstd::cerrといった文字列出力関数はマルチスレッドで出力した場合に、その出力順序が保証されないため、複数スレッドで同時に文字列を出力すると、出力結果が混ざってしまいます。

それを回避するために、Mutexによる排他制御を行います。
Mutexを使うことで、ログ出力関数が呼ばれると、呼び終わるまでは別スレッドからの呼び出しが待機されるため、同時に呼ばれることがなくなり、行単位ではログが混ざらなくなります。
なお、複数行でのログ出力では混ざる可能性があるので別途排他処理が必要になります。

【Logger.h】

#include <mutex>

/* Loggerクラス宣言 */
class Logger
{
    /* 省略 */
private:
    /* ミューテックス */
    std::mutex m_Mutex;
}

【Logger.cpp】

/* コンストラクタ */
Logger::Logger()
    : m_Mutex()
{
}

/* Informationログ出力 */
void Logger::LogInfo(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* ログ出力 */
}

/* Warningログ出力 */
void Logger::LogWarn(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* ログ出力 */
}

/* Errorログ出力 */
void Logger::LogError(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* ログ出力 */
}
4. 制御コードによる文字色変更

std::coutなどを使って、コンソール似文字列を出力する際には、制御コードを挟むことによって、文字色を変更することが出来ます。

制御コード 機能 備考
\x1b[0m 制御コードリセット
\x1b[30m 文字色を黒色に変更
\x1b[31m 文字色を赤色に変更
\x1b[32m 文字色を緑色に変更
\x1b[33m 文字色を黄色に変更
\x1b[34m 文字色を青色に変更
\x1b[35m 文字色をマゼンタに変更
\x1b[36m 文字色をシアンに変更
\x1b[37m 文字色を白色に変更


今回は、以下のように文字色を変更して出力するようにしています。

  • Information : シアン
  • Warning : 黄色
  • Error : 赤色


【Logger.cpp】

#include <iostream>

/* コンソール出力文字色制御コード */
constexpr const char* ConsoleColorReset     = "\x1b[0m";
constexpr const char* ConsoleColorBlack     = "\x1b[30m";
constexpr const char* ConsoleColorRed       = "\x1b[31m";
constexpr const char* ConsoleColorGreen     = "\x1b[32m";
constexpr const char* ConsoleColorYellow    = "\x1b[33m";
constexpr const char* ConsoleColorBlue      = "\x1b[34m";
constexpr const char* ConsoleColorMagenta   = "\x1b[35m";
constexpr const char* ConsoleColorCyan      = "\x1b[36m";
constexpr const char* ConsoleColorWhite     = "\x1b[37m";

/* Informationログ出力 */
void Logger::LogInfo(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Informationログを生成 */
    std::string log_message = StringFormat("[INFO] %s", message);

    /* Informationログを標準出力に出力(シアン) */
    std::cout << ConsoleColorCyan << log_message << ConsoleColorReset << std::endl;
}

/* Warningログ出力 */
void Logger::LogWarn(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Warningログを生成 */
    std::string log_message = StringFormat("[WARN] %s", message);

    /* Warningログを標準出力に出力(黄色) */
    std::cerr << ConsoleColorYellow << log_message << ConsoleColorReset << std::endl;
}

/* Errorログ出力 */
void Logger::LogError(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Errorログを生成 */
    std::string log_message = StringFormat("[ERROR] %s", message);

    /* Errorログを標準出力に出力(赤色) */
    std::cerr << ConsoleColorRed << log_message << ConsoleColorReset << std::endl;
}


5. ファイルへのログ出力

ファイルへのログ出力はFileLoggerクラス経由で行います。

【Logger.h】

#pragma once
#include "FileLogger.h"

/* Loggerクラス宣言 */
class Logger
{
    /* 省略 */
private:
    /* File Loggerクラスインスタンス */
    FileLogger m_FileLogger;
};


【Logger.cpp】

/* 省略 */

/* コンストラクタ */
Logger::Logger()
    : m_Mutex()
    , m_FileLogger()
{
    /* ログファイルオープン */
    this->m_FileLogger.Open("log.txt");
}

/* デストラクタ */
Logger::~Logger()
{
    /* ログファイルクローズ */
    this->m_FileLogger.Close();
}

/* Informationログ出力 */
void Logger::LogInfo(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Informationログを生成 */
    std::string log_message = StringFormat("[INFO] %s", message);

    /* Informationログを標準出力に出力(シアン) */
    std::cout << ConsoleColorCyan << log_message << ConsoleColorReset << std::endl;

    /* Informationログをファイルに出力 */
    this->m_FileLogger.Write(log_message);
}

/* Warningログ出力 */
void Logger::LogWarn(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Warningログを生成 */
    std::string log_message = StringFormat("[WARN] %s", message);

    /* Warningログを標準出力に出力(黄色) */
    std::cerr << ConsoleColorYellow << log_message << ConsoleColorReset << std::endl;

    /* Warningログをファイルに出力 */
    this->m_FileLogger.Write(log_message);
}

/* Errorログ出力 */
void Logger::LogError(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Errorログを生成 */
    std::string log_message = StringFormat("[ERROR] %s", message);

    /* Errorログを標準出力に出力(赤色) */
    std::cerr << ConsoleColorRed << log_message << ConsoleColorReset << std::endl;

    /* Errorログをファイルに出力 */
    this->m_FileLogger.Write(log_message);
}


コード全文

コード全文を示します。

【Logger.h】

#pragma once
#include "StringFormat.h"
#include "FileLogger.h"

#include <string>
#include <mutex>

/* Loggerクラス宣言 */
class Logger
{
public:
    /* 書式指定Informationログ */
    template<typename ... Args>
    static void Info(const std::string& format, Args&& ... args)
    {
        /* 書式フォーマットしてシングルトンインスタンスのInformationログ出力呼び出し */
        Logger::GetInstance().LogInfo(StringFormat(format, std::forward<Args>(args) ...));
    }

    /* 書式指定Warningログ */
    template<typename ... Args>
    static void Warn(const std::string& format, Args&& ... args)
    {
        /* 書式フォーマットしてシングルトンインスタンスのWarningログ出力呼び出し */
        Logger::GetInstance().LogWarn(StringFormat(format, std::forward<Args>(args) ...));
    }

    /* 書式指定Errorログ */
    template<typename ... Args>
    static void Error(const std::string& format, Args&& ... args)
    {
        /* 書式フォーマットしてシングルトンインスタンスのErrorログ出力呼び出し */
        Logger::GetInstance().LogError(StringFormat(format, std::forward<Args>(args) ...));
    }

private:
    /* シングルトンインスタンス取得 */
    static Logger& GetInstance();

private:
    /* コンストラクタ */
    Logger();
    /* デストラクタ */
    ~Logger();

public:
    /* コピーコンストラクタを削除 */
    Logger(const Logger&) = delete;
    /* ムーブコンストラクタを削除 */
    Logger(Logger&&) = delete;
    /* コピー代入オペレータを削除 */
    Logger& operator=(const Logger&) = delete;
    /* ムーブ代入オペレータを削除 */
    Logger& operator=(Logger&&) = delete;

private:
    /* Informationログ出力 */
    void LogInfo(const std::string& message);
    /* Warningログ出力 */
    void LogWarn(const std::string& message);
    /* Errorログ出力 */
    void LogError(const std::string& message);

private:
    /* ミューテックス */
    std::mutex m_Mutex;

    /* File Loggerクラスインスタンス */
    FileLogger m_FileLogger;
};

【Logger.cpp】

#include "Logger.h"
#include <iostream>

/* コンソール出力文字色制御コード */
constexpr const char* ConsoleColorReset     = "\x1b[0m";
constexpr const char* ConsoleColorBlack     = "\x1b[30m";
constexpr const char* ConsoleColorRed       = "\x1b[31m";
constexpr const char* ConsoleColorGreen     = "\x1b[32m";
constexpr const char* ConsoleColorYellow    = "\x1b[33m";
constexpr const char* ConsoleColorBlue      = "\x1b[34m";
constexpr const char* ConsoleColorMagenta   = "\x1b[35m";
constexpr const char* ConsoleColorCyan      = "\x1b[36m";
constexpr const char* ConsoleColorWhite     = "\x1b[37m";


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

/* コンストラクタ */
Logger::Logger()
    : m_Mutex()
    , m_FileLogger()
{
    /* ログファイルオープン */
    this->m_FileLogger.Open("log.txt");
}

/* デストラクタ */
Logger::~Logger()
{
    /* ログファイルクローズ */
    this->m_FileLogger.Close();
}

/* Informationログ出力 */
void Logger::LogInfo(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Informationログを生成 */
    std::string log_message = StringFormat("[INFO] %s", message);

    /* Informationログを標準出力に出力(シアン) */
    std::cout << ConsoleColorCyan << log_message << ConsoleColorReset << std::endl;

    /* Informationログをファイルに出力 */
    this->m_FileLogger.Write(log_message);
}

/* Warningログ出力 */
void Logger::LogWarn(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Warningログを生成 */
    std::string log_message = StringFormat("[WARN] %s", message);

    /* Warningログを標準出力に出力(黄色) */
    std::cerr << ConsoleColorYellow << log_message << ConsoleColorReset << std::endl;

    /* Warningログをファイルに出力 */
    this->m_FileLogger.Write(log_message);
}

/* Errorログ出力 */
void Logger::LogError(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Errorログを生成 */
    std::string log_message = StringFormat("[ERROR] %s", message);

    /* Errorログを標準出力に出力(赤色) */
    std::cerr << ConsoleColorRed << log_message << ConsoleColorReset << std::endl;

    /* Errorログをファイルに出力 */
    this->m_FileLogger.Write(log_message);
}


使用サンプル

Loggerクラスを使用したサンプルコードを示します。

/* Informationログ出力 */
Logger::Info("Information Log");

std::string warning_message = "warning text";

/* Warningログ出力 */
Logger::Warn("Warning Log : %s", warning_message);

std::string error_message = "error text";

int error_code = -1;

/* Errorログ出力 */
Logger::Error("Error Log : %s %d", error_message, error_code);


実行結果は以下のようになります。

Information Log
Warning Log : warning text
Error Log : error text -1

実際にコンソールに出力される文字列は色が変更されていると思います。
また、同じ内容がファイルにも出力されていると思います。

C++でスタックトレース(関数コール履歴)の取得

まえがき

今回は、前回に引き続き、C++で例外発生箇所と状況を特定するために、スタックトレースを取得する方法を実装します。

前提条件

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

OS Ver Compiler Remarks
Windows 10 Visual Studio 2019
macOS 10.15.6 Clang 11.0
Ubuntu 18.04 GCC 7.5.0


※ 今回実装するスタックトレースの取得方法は、OSやコンパイラといった環境に大きく左右されます。同じOS・コンパイラであってもバージョンによって異なる可能性もありますので、ご了承ください。

スタックトレースの取得

インタフェース

まずは、スタックトレースとして取得する情報をまとめた構造体StackTraceを定義します。

/* アドレス型 */
using address_t = void*;

/* スタックトレース情報 */
struct StackTrace
{
    /* トレース数 */
    uint32_t trace_size;
    /* トレースアドレスリスト */
    std::vector<address_t> traces;
    /* トレースシンボルリスト */
    std::vector<std::string> symbols;
};


StackTrace構造体には、トレースした関数のアドレスリスト、関数のアドレスに対応するシンボル(関数)名、トレースした関数の数を保持します。

次に、StackTrace構造体を取得するクラスStackTracerを定義します。

/* Stack Tracerクラス宣言 */
class StackTracer
{
public:
    /* スタックトレース情報取得 */
    static const StackTrace GetStackTrace();
};


スタックトレースの取得方法は、OSやコンパイラによって異なるため、ターゲットごとに実装していきます。

Windows

Windowsの場合は、ImageHlpライブラリを使うことで、スタックトレースおよび、シンボル情報を取得することが出来ます。

#include <Windows.h>
#include <ImageHlp.h>

#include <string>

#pragma comment(lib, "imagehlp.lib")

/* スタックトレース情報取得 */
const StackTrace StackTracer::GetStackTrace()
{
    /* 最大トレースサイズ */
    constexpr size_t MaxSize = 256;
    /* トレースリスト */
    void* traces[MaxSize] = {};

    /* 現在のプロセスを取得 */
    HANDLE process = GetCurrentProcess();

    /* シンボルハンドラの初期化 */
    SymInitialize(process, NULL, TRUE);

    /* スタックトレースの取得 */
    uint16_t trace_size = CaptureStackBackTrace(0, MaxSize, traces, NULL);

    /* シンボル名最大サイズをセット */
    constexpr size_t MaxNameSize = 255;
    /* シンボル情報サイズを算出 */
    constexpr size_t SymbolInfoSize = sizeof(SYMBOL_INFO) + ((MaxNameSize + 1) * sizeof(char));

    /* シンボル情報のメモリ確保 */
    SYMBOL_INFO* symbol = (SYMBOL_INFO*)calloc(SymbolInfoSize, 1);

    /* スタックトレース情報生成 */
    StackTrace stack_trace;

    /* シンボル情報メモリ確保成功 */
    if (symbol != nullptr)
    {
        /* シンボル名最大サイズをセット */
        symbol->MaxNameLen = MaxNameSize;
        /* シンボル情報サイズをセット */
        symbol->SizeOfStruct = sizeof(SYMBOL_INFO);

        /* トレースサイズをセット */
        stack_trace.trace_size = (uint32_t)trace_size;
        /* トレースリストのメモリ確保 */
        stack_trace.traces.reserve((size_t)trace_size);
        /* シンボルリストのメモリ確保 */
        stack_trace.symbols.reserve((size_t)trace_size);

        /* トレースサイズ分ループ */
        for (uint16_t i = 0; i < trace_size; i++)
        {
            /* トレースアドレスからシンボル情報を取得 */
            SymFromAddr(process, (DWORD64)(traces[i]), 0, symbol);

            /* トレースアドレスをトレースリストに追加 */
            stack_trace.traces.push_back(traces[i]);

            /* シンボル名をシンボルリストに追加 */
            stack_trace.symbols.push_back(std::string(symbol->Name));
        }

        /* シンボル情報のメモリ解放 */
        free(symbol);
    }
    else
    {
        /* Nothing to do */
    }

    return stack_trace;
}


Mac / Linux

MacLinuxの場合には、execinfoライブラリを使うことでスタックトレースおよび、シンボル情報を取得することが出来ます。

#include <execinfo.h>
#include <string>
#include <iostream>

/* スタックトレース情報取得 */
const StackTrace StackTracer::GetStackTrace()
{
    /* 最大トレースサイズ */
    constexpr size_t MaxSize = 256;
    /* トレースリスト */
    void* traces[MaxSize] = {};

    /* スタックトレース取得 */
    int trace_size = backtrace(traces, MaxSize);
    /* シンボルリスト取得 */
    char** symbols = backtrace_symbols(traces, trace_size);

    /* スタックトレース情報生成 */
    StackTrace stack_trace;

    /* トレースサイズ  */
    stack_trace.trace_size = (uint32_t)trace_size;

    /* トレースリストメモリ確保 */
    stack_trace.traces.reserve(trace_size);
    /* シンボルリストメモリ確保 */
    stack_trace.symbols.reserve(trace_size);

    /* トレースサイズ分ループ */
    for (int i = 0; i < trace_size; i++)
    {
        /* トレースアドレスをリストに追加 */
        stack_trace.traces.push_back(traces[i]);

        /* シンボル情報をシンボルリストに追加 */
        stack_trace.symbols.push_back(symbols[i]);
    }

    /* シンボルリスト解放 */
    free(symbols);

    return stack_trace;
}


未対応OS版

未対応OSの場合は、空のStackTraceを返すようにします。

/* スタックトレース情報取得 */
const StackTrace StackTracer::GetStackTrace()
{
    /* 空のスタックトレース情報生成 */
    StackTrace stack_trace;
    stack_trace.trace_size = 0;
    staci_trace.traces.clear();
    stack_trace.symbols.clear();

    return stack_trace;
}


シンボル情報のデマングル

Mac / Linux版で取得したシンボル情報は、そのままだと以下のように出力されます(※コンパイラのバージョンなどにより異なる場合があります)。

Mac版(Apple Clang 11.0)】

0   Test.out                            0x0000000104f20271 _ZN11StackTracer13GetStackTraceEv + 97
1   Test.out                            0x0000000104f1a2a7 _ZN9exception12AppException20GenerateErrorMessageEv + 55
2   Test.out                            0x0000000104f1a841 _ZN9exception12AppExceptionC2ERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_S9_i + 129
3   Test.out                            0x0000000104f1a905 _ZN9exception12AppExceptionC1ERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_S9_i + 53
4   Test.out                            0x0000000104f195ce _ZL9TestFunc2v + 126
5   Test.out                            0x0000000104f19549 _ZL9TestFunc1v + 9
6   Test.out                            0x0000000104f184a9 _ZL13ExceptionTestv + 9
7   Test.out                            0x0000000104f181ed main + 61
8   libdyld.dylib                       0x00007fff72982cc9 start + 1
9   ???                                 0x0000000000000001 0x0 + 1


Linux版(GCC 7.5)】

/<省略/Test.out(_ZN11StackTracer13GetStackTraceEv+0x58) [0x5648b25adbe8]
/<省略>/Test.out(_ZN9exception12AppException20GenerateErrorMessageB5cxx11Ev+0x3e) [0x5648b25ac77e]
/<省略>/Test.out(_ZN9exception12AppExceptionC2ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES8_S8_i+0xa4) [0x5648b25ac604]
/<省略>/Test.out(+0x872f) [0x5648b25ab72f]
/<省略>/Test.out(+0x866d) [0x5648b25ab66d]
/<省略>/Test.out(+0x8661) [0x5648b25ab661]
/<省略>/Test.out(main+0x38) [0x5648b25ab382]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7fd7fe637b97]
/<省略>/Test.out(_start+0x2a) [0x5648b25ab26a]


上記のように、そのままのシンボル情報では、関数名に当たる部分が_ZN11StackTracer13GetStackTraceEvといったコンパイラによってマングリング(名前修飾)された状態の名前で出力されます。
このままでも分からなくもないですが、デマングルしてより分かりやすい関数名にして出力したいと思います。

デマングルを行うライブラリはいくつかあるみたいですが、今回はcxxabi.hライブラリに定義されている、abi::__cxa_demangle関数を用いてデマングルを行います。

まずは、上記文字列を分解して、以下の情報を取り出します。

  • オブジェクト名
  • マングリングされたシンボル名
  • アドレス
  • アドレスオフセット

取り出した情報は、シンボル情報として構造体SymbolInfoに格納することにします。

/* シンボル情報 */
struct SymbolInfo
{
    /* シンボル情報有効フラグ */
    bool is_valid;
    /* オブジェクト名 */
    std::string object_name;
    /* アドレス */
    std::string address;
    /* マングルされたシンボル名 */
    std::string mangled_symbol_name;
    /* オフセット */
    std::string offset;

    /* コンストラクタ */
    SymbolInfo()
        : is_valid(false)
        , object_name("")
        , address("")
        , mangled_symbol_name("")
        , offset("")
    {
        /* Nothing to do */
    }
};


シンボル情報をテキストから抽出する関数は、環境ごとに正規表現を使って実装します。

/* Mac(Apple Clang 11.0の場合) */
#if TARGET_TYPE == TARGET_MAC
/* シンボル情報取得 */
inline SymbolInfo GetSymbolInfo(const std::string& raw_symbol_info)
{
    /* シンボル情報パターン : <Depth> <ObjectName> <Address> <MangledSymbolName> + <Offset> */
    std::regex pattern("^(\\d+)\\s+([\\w.\\?]+)\\s+(0x[0-9A-Fa-f]+)\\s+(\\w+)\\s+\\+\\s+(\\d+)$");
    std::smatch sm;

    /* シンボル情報生成 */
    SymbolInfo symbol_info;

    /* シンボル情報パターンにマッチ */
    if (std::regex_match(raw_symbol_info, sm, pattern))
    {
        /* 有効なシンボル情報生成 */
        symbol_info.object_name = sm[2].str();
        symbol_info.address = sm[3].str();
        symbol_info.mangled_symbol_name = sm[4].str();
        symbol_info.offset = sm[5].str();
        symbol_info.is_valid = true;
    }
    else
    {
        /* 無効なシンボル情報生成 */
        symbol_info.object_name = "";
        symbol_info.address = "";
        symbol_info.mangled_symbol_name = "";
        symbol_info.offset = "";
        symbol_info.is_valid = false;
    }

    return symbol_info;
}

/* Linux(GCC 7.5.0の場合) */
#elif TARGET_TYPE == TARGET_LINUX
/* シンボル情報取得 */
inline SymbolInfo GetSymbolInfo(const std::string& raw_symbol_info)
{
    /* シンボル情報パターン : <ObjectName>(<MangledSymbolName>+<Offset>) [<Address>] */
    std::regex pattern("^(.+)\\((\\w*)\\+(0x[0-9a-fA-F]+)\\)\\s+\\[(0x[0-9a-fA-F]+)\\]$");
    std::smatch sm;

    /* シンボル情報生成 */
    SymbolInfo symbol_info;

    /* シンボル情報パターンにマッチ */
    if (std::regex_match(raw_symbol_info, sm, pattern))
    {
        /* 有効なシンボル情報生成 */
        symbol_info.object_name = sm[1].str();
        symbol_info.mangled_symbol_name = sm[2].str();
        symbol_info.offset = sm[3].str();
        symbol_info.address = sm[4].str();
        symbol_info.is_valid = true;
    }
    else
    {
        /* 無効ななシンボル情報生成 */
        symbol_info.object_name = "";
        symbol_info.address = "";
        symbol_info.mangled_symbol_name = "";
        symbol_info.offset = "";
        symbol_info.is_valid = false;
    }

    return symbol_info;
}

/* 上記以外(コンパイラ不明)の場合 */
#else
/* シンボル情報取得 */
inline SymbolInfo GetSymbolInfo(const std::string& raw_symbol_info)
{
    /* 無効なシンボル情報を生成 */
    SymbolInfo symbol_info;
    symbol_info.object_name = "";
    symbol_info.address = "";
    symbol_info.mangled_symbol_name = "";
    symbol_info.offset = "";
    symbol_info.is_valid = false;
    return symbol_info;
}
#endif


抽出・生成したシンボル情報を元に、シンボルリストに登録する文字列を生成します。
その際に、マングリングされたシンボル名をabi::__cxa_demangle関数を使ってデマングルされた関数名に変換します。

また、デマングルに失敗した場合は、マングリングされたシンボル名をそのまま使うようにし、シンボル情報の生成に失敗した場合には、オリジナルのシンボル情報をそのまま返すようにしています。

/* シンボル情報を整形して取得 */
inline std::string GetSymbolInfoText(const std::string& raw_symbol_info)
{
    /* シンボル情報を取得 */
    const SymbolInfo symbol_info = GetSymbolInfo(raw_symbol_info);

    /* シンボル情報テキストを初期化 */
    std::string symbol_info_text = "";

    /* シンボル情報が有効 */
    if (symbol_info.is_valid == true)
    {
        /* マングルされたシンボル名を取得 */
        std::string mangled_symbol_name = symbol_info.mangled_symbol_name;

        /* マングルされたシンボル名を判定 */
        if (mangled_symbol_name[0] == '_')
        {
            /* デマングルされたシンボル名を取得 */
            int status = 0;
            char* demangled_symbol_name = abi::__cxa_demangle(mangled_symbol_name.c_str(), 0, 0, &status);

            /* デマングルに成功したらデマングルされたシンボル名を関数名にセット、失敗したらマングルされたシンボル名を使用 */
            std::string function_name = (status == 0) ? demangled_symbol_name : mangled_symbol_name;

            /* デマングルされたシンボル名のメモリ解放 */
            free(demangled_symbol_name);

            /* シンボル情報テキストをセット */
            symbol_info_text = StringFormat("%s %s + %s", symbol_info.object_name, function_name, symbol_info.offset);
        }
        else
        {
            /* シンボル情報テキストをセット */
            symbol_info_text = StringFormat("%s %s + %s", symbol_info.object_name, mangled_symbol_name, symbol_info.offset);
        }
    }
    else
    {
        /* シンボル情報テキストをセット */
        symbol_info_text = raw_symbol_info;
    }

    return symbol_info_text;
}


あとは、backtrace_symbols関数で取得したシンボル情報を、上記関数で変換し、シンボルリストに登録するように変更すれば完成です。

/* シンボル情報を取得 */
std::string raw_symbol_info = symbols[i];

#if DEMANGLE_SYMBOL_ENABLED == 1
/* シンボル情報を整形して取得 */
std::string symbol_info = GetSymbolInfoText(raw_symbol_info);
#else
/* シンボル情報をそのまま取得 */
std::string symbol_info = raw_symbol_info;
#endif

/* シンボル情報をシンボルリストに追加 */
stack_trace.symbols.push_back(symbol_info);


StackTracerクラスのソース全文を以下にに示します。

【StackTracer.cpp】

#include "StackTracer.h"
#include "CompileSwitch.h"
#include "StringFormat.h"

#if TARGET_TYPE == TARGET_MSVC
#include <Windows.h>
#include <ImageHlp.h>

#include <string>

#pragma comment(lib, "imagehlp.lib")

/* スタックトレース情報取得 */
const StackTrace StackTracer::GetStackTrace()
{
    /* 最大トレースサイズ */
    constexpr size_t MaxSize = 256;
    /* トレースリスト */
    void* traces[MaxSize] = {};

    /* 現在のプロセスを取得 */
    HANDLE process = GetCurrentProcess();

    /* シンボルハンドラの初期化 */
    SymInitialize(process, NULL, TRUE);

    /* スタックトレースの取得 */
    uint16_t trace_size = CaptureStackBackTrace(0, MaxSize, traces, NULL);

    /* シンボル名最大サイズをセット */
    constexpr size_t MaxNameSize = 255;
    /* シンボル情報サイズを算出 */
    constexpr size_t SymbolInfoSize = sizeof(SYMBOL_INFO) + ((MaxNameSize + 1) * sizeof(char));

    /* シンボル情報のメモリ確保 */
    SYMBOL_INFO* symbol = (SYMBOL_INFO*)calloc(SymbolInfoSize, 1);

    /* スタックトレース情報生成 */
    StackTrace stack_trace;

    /* シンボル情報メモリ確保成功 */
    if (symbol != nullptr)
    {
        /* シンボル名最大サイズをセット */
        symbol->MaxNameLen = MaxNameSize;
        /* シンボル情報サイズをセット */
        symbol->SizeOfStruct = sizeof(SYMBOL_INFO);

        /* トレースサイズをセット */
        stack_trace.trace_size = (uint32_t)trace_size;
        /* トレースリストのメモリ確保 */
        stack_trace.traces.reserve((size_t)trace_size);
        /* シンボルリストのメモリ確保 */
        stack_trace.symbols.reserve((size_t)trace_size);

        /* トレースサイズ分ループ */
        for (uint16_t i = 0; i < trace_size; i++)
        {
            /* トレースアドレスからシンボル情報を取得 */
            SymFromAddr(process, (DWORD64)(traces[i]), 0, symbol);

            /* トレースアドレスをトレースリストに追加 */
            stack_trace.traces.push_back(traces[i]);

            /* シンボル名をシンボルリストに追加 */
            stack_trace.symbols.push_back(std::string(symbol->Name));
        }

        /* シンボル情報のメモリ解放 */
        free(symbol);
    }
    else
    {
        /* Nothing to do */
    }

    return stack_trace;
}

#elif TARGET_TYPE == TARGET_MAC || TARGET_TYPE == TARGET_LINUX

#include <execinfo.h>
#include <string>
#include <iostream>
#include <regex>

/* シンボルのデマングル有効化 */
#define DEMANGLE_SYMBOL_ENABLED     (1)

#if DEMANGLE_SYMBOL_ENABLED == 1
#include <cxxabi.h>
#include <typeinfo>

/* シンボル情報 */
struct SymbolInfo
{
    /* シンボル情報有効フラグ */
    bool is_valid;
    /* オブジェクト名 */
    std::string object_name;
    /* アドレス */
    std::string address;
    /* マングルされたシンボル名 */
    std::string mangled_symbol_name;
    /* オフセット */
    std::string offset;

    /* コンストラクタ */
    SymbolInfo()
        : is_valid(false)
        , object_name("")
        , address("")
        , mangled_symbol_name("")
        , offset("")
    {
        /* Nothing to do */
    }
};

#if TARGET_TYPE == TARGET_MAC
/* シンボル情報取得 */
inline SymbolInfo GetSymbolInfo(const std::string& raw_symbol_info)
{
    /* シンボル情報パターン : <Depth> <ObjectName> <Address> <MangledSymbolName> + <Offset> */
    std::regex pattern("^(\\d+)\\s+([\\w.\\?]+)\\s+(0x[0-9A-Fa-f]+)\\s+(\\w+)\\s+\\+\\s+(\\d+)$");
    std::smatch sm;

    /* シンボル情報生成 */
    SymbolInfo symbol_info;

    /* シンボル情報パターンにマッチ */
    if (std::regex_match(raw_symbol_info, sm, pattern))
    {
        /* 有効なシンボル情報生成 */
        symbol_info.object_name = sm[2].str();
        symbol_info.address = sm[3].str();
        symbol_info.mangled_symbol_name = sm[4].str();
        symbol_info.offset = sm[5].str();
        symbol_info.is_valid = true;
    }
    else
    {
        /* 無効なシンボル情報生成 */
        symbol_info.object_name = "";
        symbol_info.address = "";
        symbol_info.mangled_symbol_name = "";
        symbol_info.offset = "";
        symbol_info.is_valid = false;
    }

    return symbol_info;
}
#elif TARGET_TYPE == TARGET_LINUX
/* シンボル情報取得 */
inline SymbolInfo GetSymbolInfo(const std::string& raw_symbol_info)
{
    /* シンボル情報パターン : <ObjectName>(<MangledSymbolName>+<Offset>) [<Address>] */
    std::regex pattern("^(.+)\\((\\w*)\\+(0x[0-9a-fA-F]+)\\)\\s+\\[(0x[0-9a-fA-F]+)\\]$");
    std::smatch sm;

    /* シンボル情報生成 */
    SymbolInfo symbol_info;

    /* シンボル情報パターンにマッチ */
    if (std::regex_match(raw_symbol_info, sm, pattern))
    {
        /* 有効なシンボル情報生成 */
        symbol_info.object_name = sm[1].str();
        symbol_info.mangled_symbol_name = sm[2].str();
        symbol_info.offset = sm[3].str();
        symbol_info.address = sm[4].str();
        symbol_info.is_valid = true;
    }
    else
    {
        /* 無効ななシンボル情報生成 */
        symbol_info.object_name = "";
        symbol_info.address = "";
        symbol_info.mangled_symbol_name = "";
        symbol_info.offset = "";
        symbol_info.is_valid = false;
    }

    return symbol_info;
}
#else
/* シンボル情報取得 */
inline SymbolInfo GetSymbolInfo(const std::string& raw_symbol_info)
{
    /* 無効なシンボル情報を生成 */
    SymbolInfo symbol_info;
    symbol_info.object_name = "";
    symbol_info.address = "";
    symbol_info.mangled_symbol_name = "";
    symbol_info.offset = "";
    symbol_info.is_valid = false;
    return symbol_info;
}
#endif

/* シンボル情報を整形して取得 */
inline std::string GetSymbolInfoText(const std::string& raw_symbol_info)
{
    /* シンボル情報を取得 */
    const SymbolInfo symbol_info = GetSymbolInfo(raw_symbol_info);

    /* シンボル情報テキストを初期化 */
    std::string symbol_info_text = "";

    /* シンボル情報が有効 */
    if (symbol_info.is_valid == true)
    {
        /* マングルされたシンボル名を取得 */
        std::string mangled_symbol_name = symbol_info.mangled_symbol_name;

        /* マングルされたシンボル名を判定 */
        if (mangled_symbol_name[0] == '_')
        {
            /* デマングルされたシンボル名を取得 */
            int status = 0;
            char* demangled_symbol_name = abi::__cxa_demangle(mangled_symbol_name.c_str(), 0, 0, &status);

            /* デマングルに成功したらデマングルされたシンボル名を関数名にセット、失敗したらマングルされたシンボル名を使用 */
            std::string function_name = (status == 0) ? demangled_symbol_name : mangled_symbol_name;

            /* デマングルされたシンボル名のメモリ解放 */
            free(demangled_symbol_name);

            /* シンボル情報テキストをセット */
            symbol_info_text = StringFormat("%s %s + %s", symbol_info.object_name, function_name, symbol_info.offset);
        }
        else
        {
            /* シンボル情報テキストをセット */
            symbol_info_text = StringFormat("%s %s + %s", symbol_info.object_name, mangled_symbol_name, symbol_info.offset);
        }
    }
    else
    {
        /* シンボル情報テキストをセット */
        symbol_info_text = raw_symbol_info;
    }

    return symbol_info_text;
}

#endif

/* スタックトレース情報取得 */
const StackTrace StackTracer::GetStackTrace()
{
    /* 最大トレースサイズ */
    constexpr size_t MaxSize = 256;
    /* トレースリスト */
    void* traces[MaxSize] = {};

    /* スタックトレース取得 */
    int trace_size = backtrace(traces, MaxSize);
    /* シンボルリスト取得 */
    char** symbols = backtrace_symbols(traces, trace_size);

    /* スタックトレース情報生成 */
    StackTrace stack_trace;

    /* トレースサイズ  */
    stack_trace.trace_size = (uint32_t)trace_size;

    /* トレースリストメモリ確保 */
    stack_trace.traces.reserve(trace_size);
    /* シンボルリストメモリ確保 */
    stack_trace.symbols.reserve(trace_size);

    /* トレースサイズ分ループ */
    for (int i = 0; i < trace_size; i++)
    {
        /* トレースアドレスをリストに追加 */
        stack_trace.traces.push_back(traces[i]);

        /* シンボル情報を取得 */
        std::string raw_symbol_info = symbols[i];

#if DEMANGLE_SYMBOL_ENABLED == 1
        /* シンボル情報を整形して取得 */
        std::string symbol_info = GetSymbolInfoText(raw_symbol_info);
#else
        /* シンボル情報をそのまま取得 */
        std::string symbol_info = raw_symbol_info;
#endif

        /* シンボル情報をシンボルリストに追加 */
        stack_trace.symbols.push_back(symbol_info);
    }

    /* シンボルリスト解放 */
    free(symbols);

    return stack_trace;
}

#else

/* スタックトレース情報取得 */
const StackTrace StackTracer::GetStackTrace()
{
    /* 空のスタックトレース情報生成 */
    StackTrace stack_trace;
    stack_trace.trace_size = 0;
    staci_trace.traces.clear();
    stack_trace.symbols.clear();

    return stack_trace;
}
#endif


例外処理への組み込み

StackTracerクラスを使ったスタックトレース取得処理を、前回作成したAppExceptionクラスに追加してみましょう。

【AppException.h】

#pragma once
#include "ExceptionBase.h"

#include <vector>

namespace exception
{
    /* Application Exceptionクラス宣言 */
    class AppException : public ExceptionBase
    {
    public:
        /* コンストラクタ */
        AppException(const std::string& message);

        /* コンストラクタ */
        AppException(const std::string& message, const std::string& file, const std::string& func, const int line);

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

        /* エラー要因を取得 */
        virtual char const* what() const noexcept override;

    private:
        /* エラーメッセージ生成 */
        const std::string GenerateErrorMessage();

    private:
        /* エラーメッセージ */
        std::string m_ErrorMessage;
    };
}

/* Application Exceptionのthrow */
#define THROW_APP_EXCEPTION(message) \
    throw exception::AppException(message, __FILE__, __FUNCTION__, __LINE__)


コンストラクタ内で、エラーメッセージを生成するために、GenerateErrorMessage関数と、メンバ変数m_ErrorMessageを追加しています。


【AppException.cpp】

#include "AppException.h"
#include "StringFormat.h"
#include "StackTracer.h"

#include <sstream>
#include <string>
#include <iomanip>
#include <iostream>

namespace exception
{
    /* コンストラクタ */
    AppException::AppException(const std::string& message)
        : ExceptionBase(message)
        , m_ErrorMessage("")
    {
        /* エラーメッセージ生成 */
        this->m_ErrorMessage = this->GenerateErrorMessage();
    }

    /* コンストラクタ */
    AppException::AppException(const std::string& message, const std::string& file, const std::string& func, const int line)
        : ExceptionBase(message, file, func, line)
        , m_ErrorMessage("")
    {
        /* エラーメッセージ生成 */
        this->m_ErrorMessage = this->GenerateErrorMessage();
    }

    /* デストラクタ */
    AppException::~AppException()
    {
        /* Nothing to do */
    }

    /* エラー要因を取得 */
    char const* AppException::what() const noexcept
    {
        /* エラー情報がある場合は、エラー情報付きメッセージを出力 */
        if (this->m_IsErrorInfoExists == true)
        {
            return this->m_ErrorMessage.c_str();
        }
        else
        {
            return this->m_Message.c_str();
        }
    }

    /* エラーメッセージ生成 */
    const std::string AppException::GenerateErrorMessage()
    {
        /* スタックトレース情報を取得 */
        StackTrace stack_trace = StackTracer::GetStackTrace();

        std::stringstream ss;

        /* エラー情報がある場合は、エラー情報付きメッセージを生成 */
        if (this->m_IsErrorInfoExists == true)
        {
            ss << StringFormat("[Application Error] %s @ %s[%s:L.%d]", this->m_Message, this->m_FunctionName, this->m_FilePath, this->m_LineNumber);
        }
        else
        {
            ss << StringFormat("[Application Error] %s", this->m_Message);
        }
        ss << std::endl;

        /* スタックトレースをダンプ */
        ss << "[Stack Traces] : " << std::endl;
        for (uint32_t i = 0; i < stack_trace.trace_size; i++)
        {
            if (i != 0)
            {
                ss << std::endl;
            }

            ss << "  ";
            ss << std::setw(16) << std::setfill('0') << std::hex << (uint64_t)stack_trace.traces[i];
            ss << " | ";
            ss << stack_trace.symbols[i];
        }
        ss << std::endl;

        return ss.str();
    }
}


上記コードでは、コンストラクタ内でGenerateErrorMessage関数を使ってエラーメッセージを生成し、m_ErrorMessageにセットしています。

what関数では、エラー情報(ファイルパスなど)の有無によって、出力するエラーメッセージを切り替えています。

GenerateErrorMessage関数では、エラーメッセージ、エラー発生箇所の他に、StackTracerクラスを使ったスタックトレースの取得および、ダンプ処理を追加しています。

実行

上記AppExceptionをthrowするとwhat関数からは以下のような情報が出力されます。

[ERROR] [Application Error] Invoke Application Exception @ TestFunc2[/<省略>/Test.cpp:L.34]
[Stack Traces] : 
  0000000109d924a1 | Test.out StackTracer::GetStackTrace() + 97
  0000000109d8c4d7 | Test.out exception::AppException::GenerateErrorMessage() + 55
  0000000109d8ca71 | Test.out exception::AppException::AppException(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, int) + 129
  0000000109d8cb35 | Test.out exception::AppException::AppException(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, int) + 53
  0000000109d8b7fe | Test.out TestFunc2() + 126
  0000000109d8b779 | Test.out TestFunc1() + 9
  0000000109d8a6d9 | Test.out ExceptionTest() + 9
  0000000109d8a41d | Test.out main + 61
  00007fff72982cc9 | libdyld.dylib start + 1
  0000000000000001 | ??? 0x0 + 1


上記から、Test.cpp 34行目のTestFunc2関数からAppException例外が発生した事がわかります。
また、TestFunc2関数がmain -> ExceptionTest -> TestFunc1 -> TestFunc2と呼ばれたときに例外が発生している事もわかります。

あとがき

前回と今回で、C++における例外発生箇所やスタックトレースの特定を行う処理を実現できました。

C++で例外発生箇所(ファイルパス/ファイル行番号/関数名)の特定

まえがき

今回は、C++で例外発生箇所を特定する方法についてです。

C++C#といったオブジェクト指向言語では、エラー処理は例外(Exception)という機構を使うことが一般的です。
C++では、例外をthrowするときにエラー情報を文字列として渡すことで、例外をcatchしたときにその文字列を参照することが出来ます。

try
{
    /* エラーメッセージを指定して例外をthrow */
    throw std::runtime_error("Error Message");
}
/* 例外をcatch */
catch(const std::exception& ex)
{
    /* エラーメッセージを標準エラー出力に表示 */
    std::cerr << ex.what() << std::endl;
}


しかし、文字列の情報だけだと、どの関数でどういう状況で例外が発生したかを特定するのが困難なことがよくあります。
C#などの場合は、スタックトレース(関数の呼出履歴)などの情報も参照することが出来ますが、C++デフォルトでは見ることが出来ません。

そこで、今回は標準で定義されている例外を拡張して、例外発生箇所やスタックトレースを取得する方法を実装してみたいと思います。

前提条件

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

OS Ver Compiler Remarks
Windows 10 Visual Studio 2019
macOS 10.15.6 Clang 11.0
Ubuntu 18.04 GCC 7.5.0


例外発生箇所の特定

まずは、例外発生箇所を特定できるような例外クラスを定義します。

ExceptionBaseクラス

ExceptionBaseクラスは、例外発生箇所を特定する情報を持った基底クラスです。 このクラスを継承した例外クラスを定義して使用します。

【ExceptionBase.h】

#pragma once

#include <exception>
#include <string>

namespace exception
{
    /* エラー情報 */
    struct ErrorInfo
    {
        /* ファイルパス */
        std::string file_path;

        /* 関数名 */
        std::string function_name;

        /* ファイル行番号 */
        int line_number;

        /* コンストラクタ */
        ErrorInfo(const std::string& file, const std::string& func, const int line)
            : file_path(file)
            , function_name(func)
            , line_number(line)
        {
            /* Nothing to do */
        }
    };

    /* Exception Baseクラス宣言 */
    class ExceptionBase : public std::exception
    {
    public:
        /* コンストラクタ */
        ExceptionBase(const std::string& message);

        /* コンストラクタ */
        ExceptionBase(const std::string& message, const std::string& file, const std::string& func, const int line);

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

        /* ファイルパスを取得 */
        const std::string& GetFilePath();

        /* 関数名を取得 */
        const std::string& GetFunctionName();

        /* ファイル行番号を取得 */
        int GetLineNumber();

        /* エラー情報を取得 */
        const ErrorInfo& GetErrorInfo();

        /* エラー要因を取得 */
        virtual char const* what() const noexcept override;

    protected:
        /* エラーメッセージ */
        std::string m_Message;
        /* ファイルパス */
        std::string m_FilePath;
        /* 関数名 */
        std::string m_FunctionName;
        /* ファイル行番号 */
        int m_LineNumber;
        /* エラー情報 */
        ErrorInfo m_ErrorInfo;
        /* エラー情報有無 */
        bool m_IsErrorInfoExists;
    };
}


【ExceptionBase.cpp】

#include "ExceptionBase.h"

#include <sstream>

namespace exception
{
    /* コンストラクタ */
    ExceptionBase::ExceptionBase(const std::string& message)
        : m_Message(message)
        , m_FilePath("")
        , m_FunctionName("")
        , m_LineNumber(0)
        , m_ErrorInfo("", "", 0)
        , m_IsErrorInfoExists(false)
    {
        /* Nothing to do */
    }

    /* コンストラクタ */
    ExceptionBase::ExceptionBase(const std::string& message, const std::string& file, const std::string& func, const int line)
        : m_Message(message)
        , m_FilePath(file)
        , m_FunctionName(func)
        , m_LineNumber(line)
        , m_ErrorInfo(file, func, line)
        , m_IsErrorInfoExists(true)
    {
        /* Nothing to do */
    }

    /* デストラクタ */
    ExceptionBase::~ExceptionBase()
    {
        /* Nothing to do */
    }

    /* ファイルパスを取得 */
    const std::string& ExceptionBase::GetFilePath()
    {
        return this->m_FilePath;
    }

    /* 関数名を取得 */
    const std::string& ExceptionBase::GetFunctionName()
    {
        return this->m_FunctionName;
    }

    /* ファイル行番号を取得 */
    int ExceptionBase::GetLineNumber()
    {
        return this->m_LineNumber;
    }

    /* エラー情報を取得 */
    const ErrorInfo& ExceptionBase::GetErrorInfo()
    {
        return this->m_ErrorInfo;
    }

    /* エラー要因を取得 */
    char const* ExceptionBase::what() const noexcept
    {
        /* エラー情報が存在する場合 */
        if (this->m_IsErrorInfoExists == true)
        {
            std::stringstream ss;

            /* エラーメッセージに、エラー発生箇所(関数名、ファイルパス、ファイル行番号)を付加 */
            ss << this->m_Message << " @ " << this->m_FunctionName << "[" << this->m_FilePath << ": L." << this->m_LineNumber << "]";

            std::string message = ss.str();

            return message.c_str();
        }
        /* エラー情報が存在しない場合 */
        else
        {
            /* エラーメッセージのみ出力 */
            return this->m_Message.c_str();
        }
    }
}

ExceptionBaseクラスでは、コンストラクタの引数としてエラーメッセージ(message)以外に、ファイルパス(file)、関数名(func)、ファイル行番号(line)を渡せるようにします。

また、各種エラー情報を取得するための公開関数(GetFilePath / GetFunctionName / GetLineNumber / GetErrorInfo)を定義しています。

更に、エラーメッセージを取得するwhat関数をオーバーライドし、エラー情報(ファイルパス、行番号、関数名)が渡されている場合は、エラーメッセージにエラー発生箇所を付加して返すようにします。


ExceptionBase継承サンプル

ExceptionBaseクラスを継承した例外クラスのサンプルとして、以下の2種類を実装します。

  • AppException
  • SocketException

基本的な方針としては、以下の方法で例外クラスを定義します。

  • ExceptionBaseクラスを継承する
  • 必要に応じて必要なパラメータを追加したコンストラクタを定義する
  • what関数をオーバーライドしてエラー情報を表すメッセージの出力をカスタマイズする
  • ファイルパス、ファイル行番号、関数名を取得するマクロを自動的に挿入するマクロ関数を定義する(★1)

★1 ファイルパス、ファイル行番号、関数名を取得するマクロはコンパイラが提供しており、それぞれ以下のマクロになります。

マクロ名 機能 備考
FILE ファイルパス(絶対パス)の取得
LINE ファイル行番号の取得
FUNCTION 関数名の取得
AppExceptionクラス

AppExceptionクラスは、汎用的に使用できる例外クラスで、ExceptionBaseをそのまま継承したクラスになります。

【AppException.h】

#pragma once
#include "ExceptionBase.h"

#include <vector>

namespace exception
{
    /* Application Exceptionクラス宣言 */
    class AppException : public ExceptionBase
    {
    public:
        /* コンストラクタ */
        AppException(const std::string& message);

        /* コンストラクタ */
        AppException(const std::string& message, const std::string& file, const std::string& func, const int line);

        /* デストラクタ */
        virtual ~AppException();
    };
}

/* Application Exceptionのthrow */
#define THROW_APP_EXCEPTION(message) \
    throw exception::AppException(message, __FILE__, __FUNCTION__, __LINE__)


【AppException.cpp】

#include "AppException.h"

namespace exception
{
    /* コンストラクタ */
    AppException::AppException(const std::string& message)
        : ExceptionBase(message)
    {
        /* Nothing to do */
    }

    /* コンストラクタ */
    AppException::AppException(const std::string& message, const std::string& file, const std::string& func, const int line)
        : ExceptionBase(message, file, func, line)
    {
        /* Nothing to do */
    }

    /* デストラクタ */
    AppException::~AppException()
    {
        /* Nothing to do */
    }
}


使用方法としては、エラー発生時に、マクロ関数THROW_APP_EXCEPTIONを呼び出すことで、呼び出し箇所のファイルパス、関数名、ファイル行番号を表すマクロが自動的に引数として渡され、エラー情報として格納されます。

そして例外をcatchしたときにwhat関数を呼び出すことで、エラー発生箇所の情報が付加されたエラーメッセージを取得することが出来ます。

【Test.cpp】

#include <iostream>
#include "StringFormat.h"
#include "AppException.h"

static void ExceptionTest();

static void TestFunc1();
static void TestFunc2();

int main()
{
    try
    {
        ExceptionTest();
    }
    catch (std::exception& ex)
    {
        std::cerr << StringFormat("[ERROR] %s", ex.what()) << std::endl;
    }
}

static void ExceptionTest()
{
    TestFunc1();
}

static void TestFunc1()
{
    TestFunc2();
}

static void TestFunc2()
{
    THROW_APP_EXCEPTION("Invoke Application Exception");
}


上記のような例の場合、実行結果は以下のようになります。

[ERROR] [Application Error] Invoke Application Exception @ TestFunc2[/<省略>/Test.cpp:L.34]


SocketExceptionクラス

SocketExceptionクラスは、ソケット通信関連処理で発生したエラーを通知する例外クラスで、ソケットAPIのエラーコードを保持することが出来ます。

【SocketException.h】

#pragma once
#include "ExceptionBase.h"

namespace exception
{
    /* Socket Exceptionクラス宣言 */
    class SocketException : public ExceptionBase
    {
    public:
        /* コンストラクタ */
        SocketException(const std::string& message, const int error_code);

        /* コンストラクタ */
        SocketException(const std::string& message, const int error_code, const std::string& file, const std::string& func, const int line);

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

        /* エラーコードを取得 */
        int GetErrorCode();

        /* エラー要因を取得 */
        virtual char const* what() const noexcept override;

    private:
        /* エラーメッセージ生成 */
        const std::string GenerateErrorMessage();

    private:
        /* エラーコード */
        int m_ErrorCode;

        /* エラーメッセージ */
        std::string m_ErrorMessage;
    };
}

/* Socket Exceptionのthrow */
#define THROW_SOCKET_EXCEPTION(message, error_code) \
    throw exception::SocketException(message, error_code, __FILE__, __FUNCTION__, __LINE__)


【SocketException.cpp】

#include "SocketException.h"
#include "StringFormat.h"

namespace exception
{
    /* コンストラクタ */
    SocketException::SocketException(const std::string& message, const int error_code)
        : ExceptionBase(message)
        , m_ErrorCode(error_code)
        , m_ErrorMessage("")
    {
        /* エラーメッセージ生成 */
        this->m_ErrorMessage = this->GenerateErrorMessage();
    }

    /* コンストラクタ */
    SocketException::SocketException(const std::string& message, const int error_code, const std::string& file, const std::string& func, const int line)
        : ExceptionBase(message, file, func, line)
        , m_ErrorCode(error_code)
        , m_ErrorMessage("")
    {
        /* エラーメッセージ生成 */
        this->m_ErrorMessage = this->GenerateErrorMessage();
    }

    /* デストラクタ */
    SocketException::~SocketException()
    {
        /* Nothing to do */
    }

    /* エラーコードを取得 */
    int SocketException::GetErrorCode()
    {
        return this->m_ErrorCode;
    }

    /* エラー要因を取得 */
    char const* SocketException::what() const noexcept
    {
        return this->m_ErrorMessage.c_str();
    }

    /* エラーメッセージ生成 */
    const std::string SocketException::GenerateErrorMessage()
    {
        std::stringstream ss;

        /* エラー情報がある場合は、エラー情報付きメッセージを生成 */
        if (this->m_IsErrorInfoExists == true)
        {
            ss << StringFormat("[Socket Error] %s : Error Code = %d @ %s[%s:L.%d]", this->m_Message, this->m_ErrorCode, this->m_FunctionName, this->m_FilePath, this->m_LineNumber);
        }
        else
        {
            ss << StringFormat("[Socket Error] %s : Error Code = %d", this->m_Message, this->m_ErrorCode);
        }

        ss << std::endl;

        return ss.str();
    }
}


使用方法としては、AppExceptionクラスと同様、エラー発生時にマクロ関数THROW_SOCKET_EXCEPTIONを呼び出すことで、呼び出し箇所のファイルパス、関数名、ファイル行番号を表すマクロが自動的に引数として渡され、エラー情報として格納されます。

その際に、ソケットAPI失敗時のエラーコードも引数として渡すことが出来ます。

そして例外をcatchしたときにwhat関数を呼び出すことで、エラー発生箇所の情報とエラーコードが付加されたエラーメッセージを取得することが出来ます。

/* UDPユニキャスト送信用ソケットオープン */
void OpenUdpUniTxSocket(const std::string& remote_ip, const uint16_t remote_port)
{
    /* UDP用ソケットをオープン */
    int socket = socket(AF_INET, SOCK_DGRAM, 0);

    /* ソケットオープン失敗時のエラー処理 */
    if (socket < 0)
    {
        /* ソケット例外送出 */
        THROW_SOCKET_EXCEPTION("UDP Unicast Tx Socket Open Failed", errno);
    }

    /* 〜省略〜 */
}


あとがき

長くなったので今回はここまでにして、次回はスタックトレースを取得する方法を実装したいと思います。