2017年6月7日 Theolizer®最新版v1.1.0へ対応するために修正しました。
最新版のソース一式をGistに置いてます。
こんにちは。田原です。
今回は少し複雑な階層構造のデータとその要素へのポインタを含む構造体の保存と回復をやってみます。
一般にポインタはファイルへ保存しても回復できません。しかし、Theolizer®はオブジェクト追跡を行うことで回復できます。
ついでに例外を使わないでエラーを受け取る方法も簡単に説明します。
1.今回のデータ構造
家計簿では、いつ何を幾らで買ったか入力し、何に幾らつかったか、お金が幾ら残っているかを集計すると思います。
そこで、その「何」を「科目(Item)」とし例えば下記のような階層構造で管理してみたいと思います。下図の各箱を科目(Item)として管理します。
次に、前回定義した取引(Trade)を少し拡張します。前回は科目(Item)を1つだけ定義してましたが、それを上記の科目(Item)へのポインタ2つとしました。
デビット(増える方)科目とクレジット(減る方)科目の2つです。例えば、お米を財布のお金で購入した場合、お米が増えて財布の現金が減りますのでお米がデビット側、財布がクレジット側となります。デビットとクレジットの2つを使うことで銀行からお金を下ろしてくるような操作を記録できます。下ろしてくる操作はデビット(増える方)が財布、クレジット(減る方)が預金となるわけです。
上記の科目ツリーと下記のような取引データのサンプルを後述のsub.cppのinitialize()関数で設定しています。
日付 | デビット科目 | 金額 | クレジット科目 | 備考 |
---|---|---|---|---|
2016年8月22日 | 家計/支出/主食/パン | 158円 | 家計/現金/財布 | 六切り1袋 |
2016年8月22日 | 家計/支出/副食/肉魚 | 269円 | 家計/現金/財布 | 鶏肉200g |
2016年8月22日 | 家計/支出/副食/野菜 | 182円 | 家計/現金/財布 | キャベツ1玉 |
2016年8月22日 | 家計/支出/副食/果物 | 113円 | 家計/現金/財布 | バナナ1房 |
2016年8月22日 | 家計/現金/預金 | 254,536円 | 家計/収入/給料 | セオライド テクノロジ |
2016年8月25日 | 家計/現金/財布 | 50,000円 | 家計/現金/預金 | 生活費 |
2016年8月25日 | 家計/支出/主食/お米 | 4,652円 | 家計/現金/財布 | こしひかり10Kg |
2016年8月25日 | 家計/支出/副食/肉魚 | 685円 | 家計/現金/財布 | 豚肉500g |
2016年8月25日 | 家計/支出/副食/野菜 | 152円 | 家計/現金/財布 | ほうれん草 |
2016年8月25日 | 家計/支出/副食/野菜 | 136円 | 家計/現金/財布 | 人参 |
2.データ構造の定義とオブジェクト追跡について
common.hにて必要な構造体を定義しています。この中で、科目(Item)は自分の親へのポインタmParentを持ってます。そして、取引(Trade)は取引した科目(Item)を記録するため、科目(Item)へのポインタmDebitItemとmCreditItemを持ってます。
これらのポインタをそのまま保存して、それを回復できることが今回の目玉です。
と言っても、特殊な操作はあまりありません。ポイント先も同じファイルへ保存していることが最大の条件です。今回は科目(Item)ツリーと取引(Trade)リストの両方を同じファイルへ保存します。上記のポインタ(mParentm, DebitItem, mCreditItem)の回復をTheolizer®にまかせています。
2-1.構造体定義
//############################################################################ // Theolizer解説用サンプル・プログラム2 // // 簡単な家計簿用共通定義 //############################################################################ #if !defined(COMMON_H) #define COMMON_H // *************************************************************************** // インクルード // *************************************************************************** // 標準ライブラリ #include <string> // Theolizerライブラリ #include <theolizer/serializer_json.h> #include <theolizer/list.h> // *************************************************************************** // 構造体定義 // *************************************************************************** #define POINTEE THEOLIZER_ANNOTATE(FS:<>Pointee) //---------------------------------------------------------------------------- // 科目管理 //---------------------------------------------------------------------------- struct Item { Item* mParent; // 親科目 std::string mName; // 科目名 bool mIsAssets; // 資産(財布や預金) int mAssetsIncrease;// 増加した資産金額 bool mDoManage; // 残高管理の有無 int mAmount; // 残高 theolizer::ListPointee<Item> mChildren; // 子科目 }; typedef theolizer::ListPointee<Item>::iterator ChildIterator; //---------------------------------------------------------------------------- // 取引記録 //---------------------------------------------------------------------------- struct Date { short mYear; // 年 char mMonth; // 月(1-12) char mDay; // 日(1-31) }; struct Trade { Date mDate; // 取引日 Item* mDebitItem; // 購入品(借方科目) int mAmount; // 金額 Item* mCreditItem; // 財布 (貸方科目) std::string mNote; // 備考 }; //---------------------------------------------------------------------------- // 家計簿 //---------------------------------------------------------------------------- struct HouseholdAccounts { Item mItemTree POINTEE; // 科目ツリー std::list<Trade> mTradeList; // 取引のリスト }; typedef std::list<Trade>::iterator TradeIterator; // *************************************************************************** // 関数群 // *************************************************************************** //---------------------------------------------------------------------------- // 初期設定 //---------------------------------------------------------------------------- void initialize(HouseholdAccounts* oHouseholdAccounts); void display( HouseholdAccounts* iHouseholdAccounts); void calculate( HouseholdAccounts* ioHouseholdAccounts); #endif
2-2.C++の線形リスト(List)
std::list<>を使っていますので簡単に説明します。
C言語を学んでいる時に線形リストを学んだ方も多いと思います。std::list<>も線形リストです。
また、theolizer::ListPointee<>については次節で説明しますが、これもstd::list<>を少しだけ拡張したものでstd::list<>とほぼ同じ機能を提供しています。
さて、リストの要素を追跡する際、要素へのポインタを使います。そのポインタpを次の要素へ進めるにはp=p->next;のようなコードを書きますね。
std::list<>の場合、このポインタpに当たる変数はiteratorです。そして、iterator変数itrのインクリメント(++itrなど)はp=p->next;と同じ操作となります。
また*itr
はC言語のポインタの場合と同様要素自体を示します。
サンプル・プロクラムではstd::list<>の下記メンバ関数を使っています。
メンバ関数 | 説明 |
---|---|
begin() | 先頭の要素へのiteratorが返却されます。 |
end() | 最後の要素の次のiteratorが返却されます。 |
push_back(要素) | 要素をリストの最後に追加します。 |
back() | 最後の要素の「参照」が返却されます。 |
2-3.theolizer::ListPointeeについて補足
Theolizer®はポインタが指すオブジェクト(変数)を追跡することでポインタをファイルへ保存したり、回復したりすることができます。このオブジェクト追跡対象とする変数を明示的に指定します。
std::list<>の各要素は追跡しません。そしてtheolizer::ListPointee<>の要素はオブジェクト追跡します。この2つの相違点はこれだけです。
2-4.POINTEEについて
common.hの25行目を見て下さい。次のようにPOINTEEマクロを定義しています。
#define POINTEE THEOLIZER_ANNOTATE(FS:<>Pointee)
これはあまり長い文があると分かりにくくなるので短縮のためのマクロです。
これを70行目で使っています。
mItemTreeは配下のmChildrenが持つmParentからポイントされます。
そして、mParentはファイルへ保存されますので、これを適切に回復するためmItemTreeをオブジェクト追跡する必要があります。
そこでPOINTEE(THEOLIZER_ANNOTATE(FS:<>Pointee)
)指定することでこの変数をオブジェクト追跡するようTheolizerへ指定しています。
3.保存/回復処理
main.cppにて今回のデータ構造の保存/回復処理と、各種設定・表示・計算関数呼び出しを行っています。
今回はオブジェクト追跡を行いますので、その処理にミスがないことを確認するため、clearTracking()を呼び出して下さい。もし、ミスがある時(ポインタを保存しているのにボイント先が保存されていないなど)、エラーが通知されます。Theolizer®はデフォルトでは例外でエラーを通知しますので、標準ではここで例外がthrowされます。
しかし、例外を発行しない方法もあります。最初にTheolizer®を用意する(コンストラクトする)時に例外を発行しないよう指示しておき、必要に応じてgetError()でエラー・チェックして下さい。エラーが見つかったら、必ずresetError()して下さい。resetError()を忘れるとTheolizer®を破棄する(デストラクトする)時にプログラムを異常終了させています。(エラー・チェック漏れを防ぐことが目的です。)
3-1.データの保存と回復、および、簡単な集計と表示
//############################################################################ // Theolizer解説用サンプル・プログラム2 // // メイン処理(データ保存/回復サンプル) //############################################################################ // *************************************************************************** // インクルード // *************************************************************************** // 標準ライブラリ #include <iostream> #include <fstream> // 共通定義 #include "common.h" // Theolizer自動生成先 #include "main.cpp.theolizer.hpp" // *************************************************************************** // メイン // *************************************************************************** int main(int argc, char* argv[]) { //---------------------------------------------------------------------------- // 家計簿設定 //---------------------------------------------------------------------------- // 保存用家計簿 HouseholdAccounts aHouseholdAccountsSave; // 初期設定(科目と取引データを設定している) initialize(&aHouseholdAccountsSave); //---------------------------------------------------------------------------- // 保存 //---------------------------------------------------------------------------- // 保存処理 { // 保存先のファイルをオープンする std::ofstream aStream("HouseholdAccounts.json"); // パラメータの意味 // aStream 出力先のファイル // theolizer::CheckMode::NoTypeCheck 型チェック無し // false PrettyPrintする(デフォルト) // true 例外を発行しない theolizer::JsonOSerializer<> js(aStream, theolizer::CheckMode::NoTypeCheck, false, true); // 家計簿をHouseholdAccounts.jsonファイルへ保存する THEOLIZER_PROCESS(js, aHouseholdAccountsSave); // オブジェクト追跡の締め js.clearTracking(); // エラー・チェック(例外を禁止している場合に必要) theolizer::ErrorInfo aErrorInfo=js.getErrorInfo(); if (aErrorInfo) { std::cout << aErrorInfo.getMessage() << std::endl; js.resetError(); // チェックしたことを通知する } } //---------------------------------------------------------------------------- // 回復 //---------------------------------------------------------------------------- // 回復用家計簿(正確に回復できていることを示すため、保存用と別領域) HouseholdAccounts aHouseholdAccountsLoad; // 回復処理 { // 回復元のファイルをオープンする std::ifstream aStream("HouseholdAccounts.json"); // パラメータの意味 // aStream 入力元のファイル // true 例外を発行しない theolizer::JsonISerializer<> js(aStream, true); // 家計簿をHouseholdAccounts.jsonファイルから回復する THEOLIZER_PROCESS(js, aHouseholdAccountsLoad); // オブジェクト追跡の締め js.clearTracking(); // エラー・チェック(例外を禁止している場合に必要) theolizer::ErrorInfo aErrorInfo = js.getErrorInfo(); if (aErrorInfo) { std::cout << aErrorInfo.getMessage() << std::endl; js.resetError(); // チェックしたことを通知する } } //---------------------------------------------------------------------------- // 内容表示と簡単な集計処理 //---------------------------------------------------------------------------- std::cout << u8"<<< 保存したサンプル・データ >>>\n"; display(&aHouseholdAccountsSave); std::cout << u8"\n\n<<< 回復したサンプル・データ >>>\n"; display(&aHouseholdAccountsLoad); std::cout << u8"\n\n<<< サンプルの集計結果 >>>\n"; calculate(&aHouseholdAccountsLoad); return 0; }
4.実行結果、および、保存されたファイル
4-1.実行結果
4-2.Theolzierにて保存されたJsonファイル
5.その他のファイル
5-1.アプリケーションのメインに当たる部分
本来の家計簿アプリケーションのメイン処理に当たる部分をsub.cppに入れてます。(保存/回復処理の説明用のサンプル・プログラムですので保存/回復部分をmain.cpp、その他の部分をsub.cppとにいれてます。)
sub.cppでは下記をおこなっています。本来の家計簿アプリケーションではウィンドウに家計簿を表示し、科目ツリーのメンテナンス、および、取引リストの入力を行い、必要に応じて集計を自動的に行います。それらの代わりにデータ設定と表示、集計を行っています。
関数 | 説明 |
---|---|
initialize() | 科目(Item)ツリーと取引(Trade)リストに冒頭で述べたデータを設定しています。 |
display() | 家計簿の内容を単純に表示しています。 |
calculate() | 家計簿を集計して表示しています。 |
5-2.CMakeLists.txt
5-3.自動生成されたソース
6.まとめ
一般にポインタを保存/回復する場合、ポインタのリンクを維持するためのIDを何にするか決めて、それを適切に保存/回復する処理を書く必要があります。
今回のサンプル・データならば科目名が重複していないのでこれをIDとして使えますが、重複を許して科目ツリーのパスをIDとする場合、面倒な設計とコーディングか必要になります。
しかし、Theolizer®を使えば、ポイント先も同じファイルへ保存することに注意を払うだけでポインタも保存/回復できます。
その分あなたのアプリケーションの肝心な部分に注力できると思います。