C++
C++11
C++Day 6

マクロとlibToolingでリフレクションにトライしてみた

More than 1 year has passed since last update.

これはC++ Advent Calendar 2017の6日目の投稿です。


はじめに

C++ではテンプレートが使えるのでマクロ(#define)の出番はかなり減ったのですが、まだまだ現役です。マクロでないとできない便利なことが結構あります。

Theolizerの開発でlibToolingを使っているため、必要に迫られてllvm/clangのソースを読むのですが、その中でよく使われている便利なテクニックをご紹介します。

そして、このテクニックとlibToolingの合せ技(Theolizerの超シュリンク)でリードオンリなリフレクションにトライしてみました。ちょっと遊べそうなものができましたので、それも合わせてご紹介します。


1.libToolingで良く見かけるマクロの使い方

libToolingのAST解析結果の関数やクラスやメンバ変数などの「宣言」要素は 各種のDecl というクラスで表現されます。リンク先の図のように膨大な数があり、それぞれにclang::Decl::Kindというenum型のシンボルが割り当てられています。

このenum型は DeclBase.h で次のように定義されています。(改行を修正)


DeclBase.hの関連部分

  enum Kind {

#define DECL(DERIVED, BASE) DERIVED,
#define ABSTRACT_DECL(DECL)
#define DECL_RANGE(BASE, START, END) first##BASE = START, last##BASE = END,
#define LAST_DECL_RANGE(BASE, START, END) first##BASE = START, last##BASE = END
#include "clang/AST/DeclNodes.inc"
};

このままでは分かりにくいのでちょっと修正。(結果は同じになります。)


DeclBase.hの関連部分

#define DECL(DERIVED, BASE) DERIVED,

#define ABSTRACT_DECL(DECL)
#define DECL_RANGE(BASE, START, END) first##BASE = START, last##BASE = END,
#define LAST_DECL_RANGE(BASE, START, END) first##BASE = START, last##BASE = END
enum Kind
{
#include "clang/AST/DeclNodes.inc"
};

つまり、Kindは "clang/AST/DeclNodes.inc" の内容そのものなのです。

では、その内容を少し追いかけてみましょう。先頭はアクセス指定です。(public:, protected:, private:のことです。)


clang/AST/DeclNodes.incの先頭付近

#ifndef ACCESSSPEC

# define ACCESSSPEC(Type, Base) DECL(Type, Base)
#endif
ACCESSSPEC(AccessSpec, Decl)
#undef ACCESSSPEC

Kindの定義時 ACCESSSPEC マクロは定義されていないので、#define ACCESSSPEC(Type, Base) DECL(Type, Base)定義が有効です。そして、ACCESSSPEC(AccessSpec, Decl)と書かれていますので、これはDECL(AccessSpec, Decl)と展開されます。

Kind定義の直前で#define DECL(DERIVED, BASE) DERIVED,と定義されていますから、このDECL()マクロはAccessSpec,と展開されます。

まとめると次のようになるわけです。



enum Kind

{
AccessSpec,
// (略)
};

これでKind::AccessSpecが0として定義されました。そして、このKindはDeclクラス内で定義されています。またunscoped Enumですから enum 名を省略できます。従って、Decl::AccessSpecが0として定義されたということです。

この調子で他のenumシンボルもどんどん定義されています。(途中、DECL_RANGE()マクロなどにより番号は飛んでいるようです。)

そして、この"clang/AST/DeclNodes.inc"は実にたくさんのところでインクルードされています。

AST解析する人にとって身近な DeclVisitor.h でも使われています。

その一つです。(上記Kind同様、順序と改行とインデントを修正してます。)


DeclVisitor.hの関連部分

#define PTR(CLASS) typename Ptr<CLASS>::type

#define DISPATCH(NAME, CLASS) return static_cast<ImplClass*>(this)->Visit##NAME(static_cast<PTR(CLASS)>(D))
#define DECL(DERIVED, BASE) case Decl::DERIVED: DISPATCH(DERIVED##Decl, DERIVED##Decl);
#define ABSTRACT_DECL(DECL)

RetTy Visit(PTR(Decl) D)
{
switch (D->getKind())
{
#include "clang/AST/DeclNodes.inc"
}
llvm_unreachable("Decl that isn't part of DeclNodes.inc!");
}

先程と同様にして#include "clang/AST/DeclNodes.inc"アクセス指定を展開すると次のようになります。


DeclVisitor.hの関連部分(展開中)

DECL(AccessSpec, Decl)


#define DECL(DERIVED, BASE) case Decl::DERIVED: DISPATCH(DERIVED##Decl, DERIVED##Decl);


DeclVisitor.hの関連部分(展開中)

case Decl::DERIVED: DISPATCH(DERIVED##Decl, DERIVED##Decl);


↓ DERIVED仮引数にAccessSpecが与えられていますので

(真の展開の順序とちょっと違うと思いますが、分かり易さのため変更してます。)


DeclVisitor.hの関連部分(展開中)

case Decl::AccessSpec: DISPATCH(AccessSpecDecl, AccessSpecDecl);


#define DISPATCH(NAME, CLASS) return static_cast<ImplClass*>(this)->Visit##NAME(static_cast<PTR(CLASS)>(D));


DeclVisitor.hの関連部分(展開中)

case Decl::AccessSpec: return static_cast<ImplClass*>(this)->VisitAccessSpecDecl(static_cast<PTR(AccessSpecDecl)>(D));


↓ #define PTR(CLASS) typename Ptr<CLASS>::type


DeclVisitor.hの関連部分(展開中)

case Decl::AccessSpec: return static_cast<ImplClass*>(this)->VisitAccessSpecDecl(static_cast<typename  Ptr<AccessSpecDecl>::type>(D));


Ptr<CLASS>::typeは文字列が短すぎて定義を見つけれなかったのですが、std::add_pointer<T>と同じものと思います。


DeclVisitor.hの関連部分(展開中)

case Decl::AccessSpec: return static_cast<ImplClass*>(this)->VisitAccessSpecDecl(static_cast<AccessSpecDecl*>(D));


まとめると、


DeclVisitor.hの関連部分の展開結果

RetTy Visit(Decl* D)

{
switch (D->getKind())
{
case Decl::AccessSpec: return static_cast<ImplClass*>(this)->VisitAccessSpecDecl(static_cast<AccessSpecDecl*>(D));
// (略)
}
llvm_unreachable("Decl that isn't part of DeclNodes.inc!");
}

つまり、「Visit関数に Decl* Dを与えたら、そのKind値によりswitchし、Dを該当のXxxxxDecl*へstatic_castして、VisitXxxxxDecl()を呼び出す」というプログラムを生成しています。

このような方法で 安全かつ高速にダウンキャストしているのですね。この辺も clang が高速な理由の一つと思います。


2.enum型に使ってみよう

libTooling 内部の解説みたいに成ってしまいましたが、気を取り直して進めます。

日常のプログラミングでは、libToolingのようなケースは比較的稀と思います。

enum型はよく使いますね。その値をデバッグ中に見たいことってよくあると思います。その時、できれば値ではなくシンボルを見たいことも少なくないです。そのためには、次のようなコードを書けばOKです。

#include <iostream>

#include <string>

enum Foo
{
bar,
baz,
qux,
quux,
corge
};

std::string to_string(Foo iFoo)
{
switch(iFoo)
{
case Foo::bar: return "Foo::bar";
case Foo::baz: return "Foo::baz";
case Foo::qux: return "Foo::qux";
case Foo::quux: return "Foo::quux";
case Foo::corge: return "Foo::corge";
}
return "Foo::<unknown>";
}
std::ostream& operator<<(std::ostream& os, Foo iFoo)
{
os << to_string(iFoo);
return os;
}

int main()
{
Foo foo = Foo::quux;
std::cout << foo << "\n";
}

Wandboxでみてみる。

しかし、これは面倒です。enum型を定義する度に似たような記述しますし、シンボルを変えるたびにメンテが必要です。デバッグ・ツールをいちいちメンテするのもどうかと思います。

そんなときこそ、テンプレートの出番ですが、残念ながらenumは一発で定義しないとだめなのでテンプレートではできません。(partial enumみたいなのがあればよいのですが。)

ということで、先程のlibToolingテクニックの出番です。


2-1.その前にちょっと文字列化マクロ

その前に文字列化するマクロをちょっと解説します。こちらはboostで見かけて以来、時々使ってます。

テンプレートではどう頑張ってもできないのが変数名の文字列を得ることです。型名はtypeidを使って得ることができますが、変数名はC++のコア言語仕様上できません。しかし、マクロならできます。これもデバッグの時便利です。

#include <iostream>

#define DEBUG(dVar) std::cout << #dVar "=" << dVar << "\n";

int main()
{
int foo = 123;
double bar = 45.67;

DEBUG(foo); // foo=123
DEBUG(bar); // bar=45.6
}

しかし、別途定義しておいたマクロを文字列にしたい時、うまく行きません。

#include <iostream>


#define FOO This_is_foo.
#define STRINGAIZE(dSymbol) #dSymbol

int main()
{
std::cout << STRINGAIZE(FOO) << "\n"; // "FOO"
}

直接、#FOOなどとしても、#はマクロ内だけで有効な演算子ですのでうまく行きません。

そこで、STRINGAIZE()マクロでその引数を文字列化しようとしたのですが、残念ながら"FOO"となってしまいます。

これはマクロが展開される順序のせいです。まずマクロを展開し、展開結果に対して再度展開します。

STRINGAIZE(FOO)#FOOなので"FOO"と展開されますから、"FOO"は文字列だからもう展開されないのです。

対処として一旦別のマクロを中継すれば、いきなり文字列になりませんので、再展開されます。

#include <iostream>


#define FOO This_is_foo.
#define STRINGAIZE(dSymbol) STRINGAIZE_I(dSymbol)
#define STRINGAIZE_I(dSymbol) #dSymbol

int main()
{
std::cout << STRINGAIZE(FOO) << "\n"; // This_is_foo.
}


2-2.enumとto_string()とostream::operator<<を一発で定義する

libToolingのテクニックを応用して(使い方が逆かも)、これら3つを一度に定義してみましょう。


enum型を定義する

#define ENUM Foo

#define ENUM_LIST()\
ELEMENT(bar)\
ELEMENT(baz)\
ELEMENT(qux)\
ELEMENT(quux)\
ELEMENT(corge)
#include "enum.inc"
#undef ENUM_LIST
#undef ENUM


enum.incは事前に用意しておく

#define STRINGIZE(...)       STRINGIZE_I(__VA_ARGS__)

#define STRINGIZE_I(...) #__VA_ARGS__ ""

// --------- 1.enum型の定義
#define ELEMENT(dSymbol) dSymbol,
enum ENUM
{
ENUM_LIST()
};
#undef ELEMENT

// --------- 2.to_string()の定義
inline std::string to_string(ENUM iEnum)
{
#define ELEMENT(dSymbol)\
case ENUM::dSymbol: \
return "\"" STRINGIZE(ENUM) "::" #dSymbol "\"";

switch(iEnum)
{
ENUM_LIST()
}
#undef ELEMENT
return "\"" STRINGIZE(ENUM) "::<unknown>\"";
}

// --------- 3.ostram::operator<<()の定義
inline std::ostream& operator<<(std::ostream& os, ENUM iEnum)
{
os << to_string(iEnum);
return os;
}

#undef STRINGIZE
#undef STRINGIZE_I


Wandboxでみてみる。

1発で定義できるのは良いのですが、自分で定義する部分の見栄えが悪く、また読みにくいですね。

正直「使えないな~」と感じます。この記事を書くにあたってこれでは「マクロ使えね~」ってなりそうで困りました。が、はたと気が付きました。もしかすると、Theolizerのドライバを使ってリフレクション(読み出しのみ)できるかも?


3.Theolizerドライバを超シュリンクしてマクロ定義を自動生成できる?

2-2.で自力で定義しなければいけなかったマクロを自動生成できると便利ですよね。

そして、TheolizerドライバはlibToolingを使って構文解析し、enumとクラスの定義を見てシリアライズ用のマクロ定義を自動生成しています。バージョン変更保存先指定型チェック機能をサポートしているので複雑です。しかし、それらをばっさり削ると結構簡単になりそうです。

現在の定義の名前リストのマクロ定義を出力し、かつ、インクルードするファイル名を指定できれば、リフレクションできる筈です。

それでもたいへんかもしれないのでおっかなびっくり取り掛かってみたら、意外に簡単に作ることができました。

折角ですのでGPLv3で公開します。名前は私の個人事業のTheoride TechnologyからとってTheorideDriverとしました。


3-2.TheorideDriverがやること

enum定義とclass/struct定義を構文解析して、ユーザが指定した位置へ各マクロ定義を出力します。

指定する際にインクルードするファイル名も同時に指定出来るようにしましたので、具体的な展開内容はユーザさんが自由に決めることができます。

この指定は次の構文で行います。

void GenerationMarkerStart(型名, char const* inc="インクルード・ファイル名");

関数宣言を流用しています。関数の実体は定義しないで下さい。当然、呼び出さないで下さいね。


3-2-1.enum型を指定した場合の自動生成

enum型を指定した場合は次のようにマクロ定義が自動生成されます。

#define THEORIDE_ENUM EnumTestvoid GenerationMarkerEnd(EnumTest);が自動生成範囲です。

enum class EnumTest

{
Symbol0,
Symbol1,
Symbol2
};

// 自動生成指示
void GenerationMarkerStart(EnumTest, char const* inc="enum.inc");
#define THEORIDE_ENUM EnumTest
#define THEORIDE_ENUM_LIST()\
THEORIDE_ELEMENT(Symbol0)\
THEORIDE_ELEMENT(Symbol1)\
THEORIDE_ELEMENT(Symbol2)
#include "enum.inc"
#undef THEORIDE_ENUM_LIST
#undef THEORIDE_ENUM
void GenerationMarkerEnd(EnumTest);


3-2-2.classやstructを指定した場合の自動生成

classやstruct型を指定した場合は次のようにマクロ定義が自動生成されます。

#define THEORIDE_CLASS Basevoid GenerationMarkerEnd(Base);が自動生成範囲です。

class Base

{
int mInt;
public:
Base(int iInt) : mInt(iInt) { }

// 自動生成指示
void GenerationMarkerStart(Base, char const* inc="intrusive.inc");
#define THEORIDE_CLASS Base
#define THEORIDE_BASE_LIST()
#define THEORIDE_ELEMENT_LIST()\
THEORIDE_ELEMENT_PRIVATE(mInt)
#include "intrusive.inc"
#undef THEORIDE_ELEMENT_LIST
#undef THEORIDE_BASE_LIST
#undef THEORIDE_CLASS
void GenerationMarkerEnd(Base);
};


3-2-3.クラスの外でも指定OK

クラスの外の場合も同様に自動生成されます。<ctime>ヘッダのstruct tmの例です。つまりWindows.hの内容など修正できないものでもpublicメンバなら触れます。

#include <ctime>


// 自動生成指示
void GenerationMarkerStart(tm, char const* inc="non_intrusive.inc");
#define THEORIDE_CLASS tm
#define THEORIDE_BASE_LIST()
#define THEORIDE_ELEMENT_LIST()\
THEORIDE_ELEMENT_PUBLIC(tm_sec)\
THEORIDE_ELEMENT_PUBLIC(tm_min)\
THEORIDE_ELEMENT_PUBLIC(tm_hour)\
THEORIDE_ELEMENT_PUBLIC(tm_mday)\
THEORIDE_ELEMENT_PUBLIC(tm_mon)\
THEORIDE_ELEMENT_PUBLIC(tm_year)\
THEORIDE_ELEMENT_PUBLIC(tm_wday)\
THEORIDE_ELEMENT_PUBLIC(tm_yday)\
THEORIDE_ELEMENT_PUBLIC(tm_isdst)
#include "non_intrusive.inc"
#undef THEORIDE_ELEMENT_LIST
#undef THEORIDE_BASE_LIST
#undef THEORIDE_CLASS
void GenerationMarkerEnd(tm);


3-2-4.派生クラスもOK

派生クラスを指定した場合は次のようにマクロ定義が自動生成されます。

class Derived : public Base

{
tm mTm;
EnumTest mPrivate;
protected:
int mProtected;
public:
long mPublic;
Derived(EnumTest iEnumTest, int iData) :
Base(iData*100),
mPrivate(iEnumTest),
mProtected(iData*10),
mPublic(iData)
{
time_t now = time(nullptr);
mTm = *localtime(&now);
}

// 自動生成指示
void GenerationMarkerStart(Derived, char const* inc="intrusive.inc");
#define THEORIDE_CLASS Derived
#define THEORIDE_BASE_LIST()\
THEORIDE_BASE_PUBLIC(Base)
#define THEORIDE_ELEMENT_LIST()\
THEORIDE_ELEMENT_PRIVATE(mTm)\
THEORIDE_ELEMENT_PRIVATE(mPrivate)\
THEORIDE_ELEMENT_PROTECTED(mProtected)\
THEORIDE_ELEMENT_PUBLIC(mPublic)
#include "intrusive.inc"
#undef THEORIDE_ELEMENT_LIST
#undef THEORIDE_BASE_LIST
#undef THEORIDE_CLASS
void GenerationMarkerEnd(Derived);
};


4.動作原理、適用方法など


4-1.動作原理

ユーザ・プロジェクトのビルド過程に割り込んで、ユーザの指示に従いソース・コードを修正します。

そのために、TheorideDriverではコンパイラを置き換えるテクニックを使うことにしました。

Theolizerのようにコンパイラを置換せずCMakeを使ってユーザ・プロジェクトへ組み込むことも可能ですが、よりお手軽に使えるものにしたかったのです。ヘッダもライブラリのリンクも不要なのですから。

Visual C++のcl.exeをリネーム(clRenamedByTheoride.exe)し、元のcl.exeの位置へTheorideDriverをコピーすることでプロジェクトのビルドに割り込みます。この置き換え操作のためのバッチ・ファイルを同梱しています。


4-2.対応コンパイラ

設計的にはマルチプラットフォームですが、現在 Visual C++ 2017でのみ動作検証しています。

gccやMinGWでも動くと思いますが、経験的には細かい部分でトラブルが起こります。多少の修正が必要な筈です。

ソースも小さいですし、もし、gccで動かすことに興味ある方がいらっしゃいましたらご自身でトライされることを応援します。判らなことがあったら聞いて頂ければ出来る限り回答します。(Theolizerのように巨大なものではありません。高々有効行2,000行ほどです。)


4-3.適用方法

GitHubのTheorideDriverリポジトリへ上げています。

ここのリリースから Install.zip をダウンロードして下さい。


4-3-1.同梱物

ダウンロードしたファイルを解凍するとInstallフォルダとtestフォルダがでてきます。この中に次の物が入っております。


  1. Installフォルダ内 replace.bat/restore.bat

    上記のTheorideDriverのリプレースとリストア用のバッチ・ファイルです。Visual C++ 2017に対応しております。Visual C++のPC用のコンパイラを全て置き換えます。分かりにくいのですが下記の4つです。

    1-1. 32ビット・ビルド用の32ビット・ビルドされたcl.exe

    1-2. 32ビット・ビルド用の64ビット・ビルドされたcl.exe

    1-3. 64ビット・ビルド用の32ビット・ビルドされたcl.exe

    1-4. 64ビット・ビルド用の64ビット・ビルドされたcl.exe


  2. Installフォルダ内 binフォルダ

    この中に「cl.exe」が1つ入っています。これがTheorideDriverです。Visual C++のcl.exeを置き換えます。64ビット・ビルドしておりますので64bit Windowsでのみ動作致します。

    他に上記のリプレース/リストアを補助するバッチ・ファイルが幾つか入っております。


  3. testフォルダ

    ここにはサンプル・プログラムを入れています。test.cppがプログラムのサンプルです。上述の3章のサンプルが入っています。


また、enum.inc、intrusive.inc、non_intrusive.incに自動生成されたマクロからインクルードするプログラムのサンプルが入っています。

enum型はシンボル名へ変換します。

クラスはJsonチックな文字列へ変換します。(手を抜いているのでJsonではないです。)


4-3-2.使い方


  1. リプレース方法

    replace.batを右クリックし、「管理者として実行(A)」をクリックして下さい。

    Visual C++のcl.exeは管理者でないとリネームできないためです。


  2. リストア方法

    restore.batを右クリックし、「管理者として実行(A)」をクリックして下さい。


  3. ユーザ・ソースの追加の修正

    上述のGenerationMarkerStart指定とcppファイル(コンパイル単位)の先頭付近で#define THEORIDE_DO_PROCESSを記述することでマクロが自動生成されます。THEORIDE_DO_PROCESSはTheorideDriverへの構文解析指示です。これがないコンパイル単位はソース自動生成しません。

    コンパイラを置き換える都合上、全てのプロジェクトで構文解析が機能すると非常に遅くなります。ですのでデフォルトでは構文解析しないようにするための仕組みです。


4.サンプルの使い方

cl.exeのリプレース後、Visual Studio 2017のソリューション・ファイルもいれていますので、これをダブルクリックしてVisual Studioを起動して下さい。後は普通にビルドするとソースが test.cpp 内の指定場所(GenerationMarkerStartのある場所)へ自動生成されコンパイルされます。


メイン・ルーチン

int main()

{
Derived aDerived(EnumTest::Symbol1, 123);
std::cout << "aDerived :" << aDerived << "\n";
}


実行結果

{

"<Class Name>":"Derived",
"(Base)":
{
"<Class Name>":"Base",
"mInt":12300
},
"mTm":
{
"<Class Name>":"tm",
"tm_sec":30,
"tm_min":19,
"tm_hour":22,
"tm_mday":5,
"tm_mon":11,
"tm_year":117,
"tm_wday":2,
"tm_yday":338,
"tm_isdst":0
},
"mPrivate":"EnumTest::Symbol1",
"mProtected":1230,
"mPublic":123
}

お手軽にバッグできそうな気がしません?


5.TheorideDriverのビルド方法

TheorideDriverをビルドするためには、Theolizerと同じく、libToolingとboostが必要になります。

boostのsystemとfilesystemのビルドが必要です。libToolingは多くのコンパイラ向けにプリビルド版が提供されているのですが、残念ながらVisual C++はありません。そこで、私の方でVisual C++ 2015でビルドしたものをここに上げています。ただし、デバッグ版は非常に巨大なことと構文解析処理が非常に遅くてあまり役に立たないで省いています。

Theolizerはこれらのダウンロードと解凍、必要なビルドを自動化していますので、そのビルド・システムを流用すると準備の手間が省けます。

そこで、これを用いた場合のTheorideDriverのビルド手順を書きます。


5-1. もし、CMakeとVisual Studio 2017をお持ちでないならご用意下さい

私のサイトの技術ブログに手順を書いたことがありますので、必要な方は参照下さい。


5-2. Theolizerの最新版のソースをzipダウンロード、もしくはクローンして下さい。


5-3. ダウンロード/解凍先フォルダのbuild_toolsにあるwindows.cmakeを編集して下さい

標準ではTheolizerが対応するWindows用全てのコンパイラの準備をしますのでそれなりに時間がかかります。

142~155行目の下記を削除して下さい。

build_by_gcc( mingw710 64 StaticWithBoost TRUE TRUE "39 1" "11 1")

build_by_gcc( mingw710 32 StaticWithBoost TRUE TRUE "39 1" "11 1")
output_title("****** Shared ******")
build_by_msvc(msvc2017 64 Shared FALSE FALSE "38 11 1 1")
build_by_msvc(msvc2017 32 Shared FALSE FALSE "38 11 1 1")
build_by_gcc( mingw710 64 Shared FALSE FALSE "38 1" "11 1")
build_by_gcc( mingw710 32 Shared FALSE FALSE "38 1" "11 1")
output_title("****** Static ******")
build_by_msvc(msvc2017 64 Static FALSE FALSE "38 11 1 1")
build_by_msvc(msvc2017 32 Static FALSE FALSE "38 11 1 1")
build_by_gcc( mingw710 64 Static FALSE FALSE "38 1" "11 1")
build_by_gcc( mingw710 32 Static FALSE FALSE "38 1" "11 1")

140か141行のどちらかを残して下さい。上が 64bitビルド用 下が32bitビルド用です。


build_by_msvc(msvc2017 64 StaticWithBoost TRUE TRUE "39 11 1 1")

build_by_msvc(msvc2017 32 StaticWithBoost TRUE TRUE "39 11 1 1")


後は同じフォルダにある zz1_config_all.bat を実行すると指定したTheolizerをビルドするためのコンフィグレーションが行われます。その時、llvmとboostが準備されます。どちらも大きいのでダウンロードと解凍にそこそこ時間がかかりますが、数分程度で完了すると思います。


5-4. TheorideDriverのダウンロード

Theolizerと同じレベルのフォルダへzipダウンロードして解凍、もしくはクローンして下さい。


5-5. 以上の手順で構築されるフォルダ構成

32bitと64bitの両方を用意した場合、次のようなイメージとなります。

フォルダ構成.png

64bitビルドする場合は赤文字で囲ったboostとllvmのパスをTheorideDriverのビルド用CMakeLists.txtへ教えます。


5-6. ビルドとテスト実行

上記赤枠のフォルダにllvmとboostがあることを前提としたコンフィグ用のバッチファイルを用意しています。zz_config.batです。これを起動すると CMake により必要な Visual Studio のソリューションが用意されます。

最後に、zz_build_test.bat を実行すると、TheorideDriverがビルドされ、サンプルの test.cpp へマクロを自動的に書き込み、コンパイルされて実行されます。この一連の処理を書いてます。

もちろん、バッチではなく Visual Studio を起動してTheorideDriverをビルドすることもできます。


6.使用上の注意

1. 自動生成部分は基本的に修正しないでください。

enum定義やクラス定義が変更された場合、自動的に反映するためにGenerationMarkerEndまでに上書きしますので、GenerationMarkerEndをソースコードの後ろの方へ移動するなどした場合、そこまでのコードが消えます。

2. なにか問題が発生した場合は、自動生成範囲をバッサリ削除して下さい。

3. コンパイル中にリプレース/リストア操作しないでください

リストアできなくなる可能性があります。もし、リストアできなくなった場合は、各フォルダに入っている clRenamedByTheoride.exe を cl.exeへ手動で戻せばリストアできます。

それがうまくできない場合は Visual Studio を修復セットアップして下さい。修復セットアップ後、Visual C++のフォルダに残っているclRenamedByTheoride.exeを削除して下さい。Visual Studio 2017を標準インストールした場合、次の4つのフォルダに入っています。

C:/Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.11.25503\bin\HostX86\x86\

C:/Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.11.25503\bin\HostX64\x64\
C:/Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.11.25503\bin\HostX64\x86\
C:/Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.11.25503\bin\HostX86\x64\

4. 信頼性はあまり期待しないで下さい*

自動テストを実装していませんし、あまり密なテストも行っていません。

更に上記のリスクがあるのでTheolizerでは怖くて踏み切れなかったユーザ・ソースの直接修正に踏み込んでます。

更に更にコンパイラの置き換えもちょっと怖いです。(ここに一番時間をかけてテストしてます。)

5. #define THEORIDE_DO_PROCESS の定義をお忘れなく

作った本人もよく忘れて動かない動かないと悩んだことがあります。ご用心。


7.最後に

今回のサンプルは配列やポインタには対応していません。test.cppの頭で基本型に対応していますが、それと同じように配列やポインタへの対応が必要な筈です。(Theolizerはそのような分岐があります。)to_string()関数テンプレートをオーバーロードすれば対応できる筈です。

STL群についてもそれぞれ用にインクルード・ファイルを用意すれば対応できるだろうと思います。仕組みが単純なだけにたぶん色々出来るはずです。

さて、サンプルはついついシリアライザっぽいものに成ってしまいましたが、これは私の頭がシリアライザ脳だからです。各インクルード・ファイルは自由に定義できますから、メンバのテーブルを定義したり、メンバ変数を追加することなどもできます。どこかのカウンタを増やすとかリンクするとかもできる筈です。色々遊べそうな気がします。

C++でのリフレクションに興味のある方がいらっしゃいましたら、遊んでやって下さい。