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.構造体定義

//############################################################################
//      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.メイン・プログラム

//############################################################################
//      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.保存された内容

{
    "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
#[[###########################################################################
        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})

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

main.cpp.theolizer.hpp
#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 ######

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.まとめ

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