2017年6月7日 Theolizer®最新版v1.1.0へ対応するために修正しました。
最新版のソース一式をGistに置いてます。

こんにちは。田原です。

今回はTheolizerで簡単な構造体の保存/回復を行うための手順を説明します。
と言ってもそれほど難しい内容はありません。

1.開発に必要とする知識について

Theolizer®はC++言語(C++11規格準拠)で開発しています。ですのでTheolizer®を適用するアプリもC++11規格対応したC++コンパイラでビルドする必要があります。ですが、アプリを開発する人がC++言語について深く知っている必要はありません。C++をベターCとしてお使いの場合でも十分使えるように設計しました。

今回のシリーズでは、なるべくC言語の知識で理解できる記述で解説したいと思います。
ただし、下記のC++機能はC言語の欠点をカバーするために有用な機能ですので使わせて下さい。基本的な使い方について簡単ですがベターCとして便利なC++機能の紹介で解説してます。また、検索すれば多数の解説サイトがありますから、必要に応じてそれらも参考にして下さい。

名前 機能
std::cout 標準出力
std::ofstream ファイル出力
std::ifstream ファイル入力
std::string 文字列
std::list<> 双方向の線形リスト

2.サンプルとして開発するアプリケーションについて

Theolizer®の使い方を説明するに当たって、対象とするアプリを想定しました。簡単な家計簿です。
ただし、あまりに単純な家計簿では面白くありませんので、科目を階層構造で定義し、各科目について、商品なら底値や必要なら在庫数量(普通は管理しないと思いますがサンプルですので)、財布や預金通帳なら残高等を管理することを目標にします。

なお、完全なアプリを開発していると時間がかかりますので、ここではデータ保存/回復部分を中心に開発・説明します。ユーザインターフェースについては特に触れません。
Theolizer®を使えば保存/回復は一発ですので、どんなデータ構造をTheolizer®が取り扱えるのか、そして、それを保存/回復するためにどのようにしてTheolizer®へ指示を与えるのかについて解説していきたいと思います。

3.シリアライズ対象となるデータ構造定義

今回は簡単な構造体を1つ保存/回復してみます。
構造体として日付構造体(Date)と取引構造体(Trade)の2つを定義します。
Dateは見ての通りです。Tradeは商品の購入を記録します。この先、「銀行からお金を下ろしてくる」などもTradeで記録するように拡張しますので少し汎用な名前を付けます。

3-1.構造体定義

main.h
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//############################################################################
//      Theolizer解説用サンプル・プログラム
//
//          簡単な家計簿用データ構造定義
//              データの保存と回復のサンプルなので、
//              ユーザ・インタフェースは実装しない
//############################################################################
 
#if !defined(MAIN_H)
#define MAIN_H
 
// ***************************************************************************
//      インクルード
// ***************************************************************************
 
// 標準ライブラリ
#include <string>
 
// ***************************************************************************
//      構造体定義
// ***************************************************************************
 
struct Date
{
    short   mYear;              // 年
    char    mMonth;             // 月(1-12)
    char    mDay;               // 日(1-31)
};
 
struct Trade
{
    Date        mDate;          // 取引日
    std::string mItem;          // 費目
    int         mAmount;        // 金額
};
 
#endif

4.データを設定し、保存/回復する

データ構造を定義した後は、一般にその構造でデータ領域を確保し、各メンバに値を設定します。
そのデータの保存と回復のコードです。

4-1.メイン・プログラム

main.cpp
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//############################################################################
//      Theolizer解説用サンプル・プログラム
//
//          簡単な家計簿用データ構造定義
//              データの保存と回復のサンプルなので、
//              ユーザ・インタフェースは実装しない
//############################################################################
 
// ***************************************************************************
//      インクルード
// ***************************************************************************
 
// 標準ライブラリ
#include <iostream>
#include <fstream>
 
// 構造体定義
#include "main.h"
 
// Theolizerライブラリ(各種シリアライズ指示する時は構造体定義前に#include)
#include <theolizer/serializer_json.h>
 
// Theolizer自動生成先
#include "main.cpp.theolizer.hpp"
 
// ***************************************************************************
//      メイン
// ***************************************************************************
 
int main(int argc, char* argv[])
{
//----------------------------------------------------------------------------
//      保存
//----------------------------------------------------------------------------
 
    // 保存するデータの設定
    Trade  aTradeSave;
    aTradeSave.mDate.mYear  = 2015;
    aTradeSave.mDate.mMonth = 1;
    aTradeSave.mDate.mDay   = 1;
    aTradeSave.mItem        = u8"お米10Kg";
    aTradeSave.mAmount      = 4600;
 
    // 保存処理
    {
        std::ofstream   aStream("test.log");
        theolizer::JsonOSerializer<>  js(aStream);
        THEOLIZER_PROCESS(js, aTradeSave);
    }
 
//----------------------------------------------------------------------------
//      回復
//----------------------------------------------------------------------------
 
    // 回復先の領域
    Trade  aTradeLoad;
 
    // 回復処理
    {
        std::ifstream   aStream("test.log");
        theolizer::JsonISerializer<>  js(aStream);
        THEOLIZER_PROCESS(js, aTradeLoad);
    }
 
    // 結果表示
    std::cout << theolizer::print("%04d/%02d/%02d %s %u\n",
        aTradeLoad.mDate.mYear,
        aTradeLoad.mDate.mMonth+0,
        aTradeLoad.mDate.mDay+0,
        aTradeLoad.mItem, aTradeLoad.mAmount);
 
    return 0;
}

保存処理は下記の4ステップです。

  1. std::ofstream wStream(“test.log”);
    test.logファイルを書き込みオープンします。

  2. theolizer::JsonOSerializer<> js(wStream);
    Theolizer®に対して上記でオープンしたファイルへシリアライズするよう指示します。

  3. THEOLIZER_PROCESS(js, wTradeSave);
    事前に設定していたwTradeSave変数の内容をtest.logファイルへ出力するよう指示しています。

  4. { }ブロック終了
    ブロックから抜けた時にファイルがクローズされて書き込みが完了します。

ところで、領域確保やメンバへの値設定はユーザ・インタフェース部で通常行います。その部分については解説の対象としませんので、今回は直接設定しています。

4-2.保存された内容

test.log
01
02
03
04
05
06
07
08
09
10
11
12
13
14
{
    "SerialzierName":"JsonTheolizer",
    "GlobalVersionNo":1,
    "TypeInfoList":[1]
}
{
    "mDate":{
        "mYear":2015,
        "mMonth":1,
        "mDay":1
    },
    "mItem":"お米10Kg",
    "mAmount":4600
}

下記の4ステップで回復されます。

1.std::ifstream   wStream(“test.log”);
test.logファイルを読み出しオープンします。

  1. theolizer::JsonISerializer<>  js(wStream);
    Theolizer®に対して上記でオープンしたファイルからデシリアライズするよう指示します。

  2. THEOLIZER_PROCESS(js, wTradeLoad);
    事前に確保していたwTradeLoad変数へtest.logファイルの内容を回復するよう指示しています。

  3. { }ブロック終了
    ブロックから抜けた時にファイルがクローズされて読み出しが完了します。

4-3.実行結果

2015/01/01 お米10Kg 4600

4-4.CMakeLists.txt

CMakeLists.txt
[text] #[[###########################################################################
Theolizer紹介用サンプルCMakeLists.txt
]]############################################################################

if("${CMAKE_VERSION}" STREQUAL "")
set(CMAKE_VERSION, 3.5.0)
endif()
cmake_minimum_required(VERSION ${CMAKE_VERSION})

message(STATUS "BOOST_ROOT=${BOOST_ROOT}")

#—————————————————————————–
# プロジェクト設定
#—————————————————————————–

set(TARGET_NAME sample)
set(SOURCE_LIST main.cpp)
set(HEADER_LIST main.h)

project(${TARGET_NAME} VERSION 1.0.0)

#—————————————————————————–
# ビルド設定
#—————————————————————————–

# MSVCの通常使わないビルド・モードとZERO_CHECKプロジェクトの削除
set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "Configs" FORCE)
set(CMAKE_SUPPRESS_REGENERATION TRUE)

# Theolizer
find_package(THEOLIZER)

# Options
if (${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC)
add_definitions(-D_UNICODE -DUNICODE)
set(CMAKE_DEBUG_POSTFIX "d")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj")
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")

# MinGWの不具合(https://sourceforge.net/p/mingw-w64/discussion/723797/thread/c6b70624/#7f0a)暫定対処
if((CMAKE_COMPILER_IS_MINGW) AND (CMAKE_SIZEOF_VOID_P EQUAL 8))
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wa,-mbig-obj")
endif()
endif()

# example
add_executable(${TARGET_NAME} ${SOURCE_LIST} ${HEADER_LIST})
setup_theolizer(${TARGET_NAME} StaticWithBoost)

#—————————————————————————–
# テスト実行
#—————————————————————————–

enable_testing()
add_test(NAME ${TARGET_NAME} COMMAND $<TARGET_FILE:${TARGET_NAME}>)

add_custom_target(BuildTest COMMAND "ctest" "-V" "-C" $<CONFIG>)
add_dependencies(BuildTest ${TARGET_NAME})
[/text]

4-5.自動生成されたソース

main.cpp.theolizer.hpp
[cpp] #ifdef THEOLIZER_WRITE_CODE // ###### Date ######

#define THEOLIZER_GENERATED_LAST_VERSION_NO THEOLIZER_INTERNAL_DEFINE(kLastVersionNo,1)
#define THEOLIZER_GENERATED_FULL_AUTO Date

// —<<< Version.1 >>>—

#define THEOLIZER_GENERATED_VERSION_NO THEOLIZER_INTERNAL_DEFINE(kVersionNo,1)
#define THEOLIZER_GENERATED_CLASS_NAME()\
THEOLIZER_INTERNAL_CLASS_NAME((u8"Date"))
#define THEOLIZER_GENERATED_ELEMENT_MAP emName
#define THEOLIZER_GENERATED_ELEMENT_LIST()\
THEOLIZER_INTERNAL_ELEMENT_N((mYear),mYear,etmDefault,\
(theolizerD::All),\
(short))\
THEOLIZER_INTERNAL_ELEMENT_N((mMonth),mMonth,etmDefault,\
(theolizerD::All),\
(char))\
THEOLIZER_INTERNAL_ELEMENT_N((mDay),mDay,etmDefault,\
(theolizerD::All),\
(char))
#include <theolizer/internal/version_auto.inc>
#undef THEOLIZER_GENERATED_VERSION_NO

#endif//THEOLIZER_WRITE_CODE // ###### Date ######

#ifdef THEOLIZER_WRITE_CODE // ###### Trade ######

#define THEOLIZER_GENERATED_LAST_VERSION_NO THEOLIZER_INTERNAL_DEFINE(kLastVersionNo,1)
#define THEOLIZER_GENERATED_FULL_AUTO Trade

// —<<< Version.1 >>>—

#define THEOLIZER_GENERATED_VERSION_NO THEOLIZER_INTERNAL_DEFINE(kVersionNo,1)
#define THEOLIZER_GENERATED_CLASS_NAME()\
THEOLIZER_INTERNAL_CLASS_NAME((u8"Trade"))
#define THEOLIZER_GENERATED_ELEMENT_MAP emName
#define THEOLIZER_GENERATED_ELEMENT_LIST()\
THEOLIZER_INTERNAL_ELEMENT_KN((mDate),mDate,etmDefault,\
(theolizerD::All),\
(Date),1)\
THEOLIZER_INTERNAL_ELEMENT_N((mItem),mItem,etmDefault,\
(theolizerD::All),\
(std::string))\
THEOLIZER_INTERNAL_ELEMENT_N((mAmount),mAmount,etmDefault,\
(theolizerD::All),\
(int))
#include <theolizer/internal/version_auto.inc>
#undef THEOLIZER_GENERATED_VERSION_NO

#endif//THEOLIZER_WRITE_CODE // ###### Trade ######

#ifdef THEOLIZER_WRITE_CODE // ###### Global VersionNo. Table ######
THEOLIZER_GENERATED_GLOBAL_TABLE();
#endif//THEOLIZER_WRITE_CODE // ###### Global VersionNo. Table ######
[/cpp]

5.インクルードするヘッダについて

main.cppで#include <theolizer/serializer_json.h>をインクルードしています。これはJsonフォーマットでシリアライズ/デシリアライズするための派生Serializerです。使いたい派生Serializerのヘッダをインクルードすることになります。現在はJsonとBinary、ちょっと特殊なFastの3種類を用意しています。

そして、main.cppで#include “main.cpp.theolizer.hpp”をインクルードしています。これはある意味Theolizer®を使うためのオマジナイです。
THEOLIZER_PROCESS()を呼び出しているcppファイルで、保存/回復したい構造体やクラスの定義の後、THEOLIZER_PROCESS()を呼び出す前にインクルードして下さい。

6.Theolizerで用いているコア技術

構造体やクラスをシリアライズ/デシリアライズするためには、それらのメンバ・リストをC++プログラムが把握する必要があるのですが、リフレクション機能のないC++単体ではそれを自動化することができません。
そこで、外部ツールで構文解析を行い、必要な情報をソース・コードとして自動生成しています。そのファイルがmain.cpp.theolizer.hppです。
その外部ツールであるTheolizer®ドライバがユーザ・プログラムのソース・コードをclangのlibToolingを用いて構文解析し、シリアライズ/デシリアライズするために必要なコードを自動生成しているのです。(リンク先の記事の著者Chironianは私です。プライベートではChironianのハンドルで活動します。)

以上により、リフレクション機能のないC++言語でもメンバを追加した際に保存/回復のためのコードを手で修正することなく保存/回復できるようになりました。

7.まとめ

以上で、簡単なデータ構造を保存/回復する使い方の説明は終わりです。単純な対象ですが、保存/回復処理を簡単に行えることを説明できたと思います。
次回以降で、もう少し複雑なデータ構造をもう少し複雑な保存/回復するための使い方を解説していきます。保存/回復のために少し工夫は必要ですが、かなり複雑なものでも実処理は数行で保存/回復できます。それが分かるような解説にする予定です。