概要
-
@xxx
のように@
を付けて型や変数の前にUDAを付けることができます。将来的な予約語との衝突は気にした方がいいです。 - UDAを使った処理を書く場合は
std.traits
のhasUDA
,getUDAs
,getSymbolsByUDA
が便利です。 - ↑が使えない場合は自分で定義します。その際は
isXXXX
,hasXXXX
,getXXXX
の3つのテンプレートを定義すると使いやすいです。
UDAとは
UDAというのは、構造体・クラスや関数に独自の属性を付与して、コンパイル時にあれこれしたいときの目印や付属データにするためのものです。
以下のように、 @xxx
のように、アットマークを付けてUDAを付与します。
struct testUda{}
// 文字列をUDAとして付与
@("test")
struct Data
{
// 定義済みの型 testUda をUDAとして付与
@testUda
void foo() {}
}
UDAになりうるものは、値や型で、AliasSeqに突っ込めるものは大体突っ込めます。なんでも突っ込めてしまうため、逆に何を突っ込むのがベストなのかですごく悩みます。こういう場合に制約を加えたり、一定のパターンを定石として構築し、考える手間を省くのは良いプラクティスでしょう。
仕様的にはカッコは不要ですが、@nogc導入時のように唐突に予約語と化す可能性があるので、カッコをつけたり、予約語になりにくそうな2語以上の組み合わせでlowerCamelCase化する、UpperCamelCaseやsnake_caseとかにするのが無難です。なお、現時点で@live
や@future
は予約語になりそうな候補です。
UDAの使い方
UDAの付与の仕方まではいいのですが、いざUDAを利用しようとすると困難が待ち構えています。
普段TMP(テンプレートメタプログラミング)に慣れ親しんでいてもなお、割かし複雑な操作が要求されます。この扱いが難しいため、備忘録的な意味も込めて、本記事の作成に至りました。
さて、UDAを扱う、最もプリミティブなものは以下になるでしょう。
pragma(msg, __traits(getAttributes, Data)[0]);
_traits(getAttributes, identifier)
で、identifierに付与されたUDAがTupleで得られます。
詳しくは公式で見てもらうとしても、おおむねこれが仕様のすべてです。
ハイ、このまま「さあ使って!」と言って使える人はレアでしょうね。
UDAを利用するための定石
UDAをうまいこと利用するためには、いくつかの定石を知っておく必要があります。
主にPhobosのstd.traitsのhasUDA, getUDAs, getSymbolsByUDA あたりを使います。
import std.traits;
struct Data
{
@("XXX")
@("YYY")
int aaabbb;
@("XXX")
int aaa;
@("YYY")
int bbb;
}
// Data.aaaは、文字列型のUDAを持っていて、1つ目は"XXX"です。
static assert(getUDAs!(Data.aaa, string)[0] == "XXX");
// Data.bbbは"YYY"のUDAを持っています。
static assert(hasUDA!(Data.bbb, "YYY"));
// Dataのメンバで、"XXX"のUDAを持っているメンバは2つです。
static assert(getSymbolsByUDA!(Data, "XXX").length == 2);
// Dataのメンバで、"YYY"のUDAをもつ1つ目のメンバは、aaabbbです。
static assert(__traits(identifier, getSymbolsByUDA!(Data, "YYY")[0]) == "aaabbb");
// Dataのメンバで、"YYY"のUDAをもつ1つ目のメンバ(aaabbb)は、"XXX"というUDAを持っています。
static assert(hasUDA!(getSymbolsByUDA!(Data, "YYY")[0], "XXX"));
ですが、実のところ、ちょっと複雑なことをしようと思うとこれだけでは心もとありません。
例えば、 @("aaa")と@("bbb")の両方とものUDAを持ってるシンボルだけ取り出したい、とか、@("aaa")と@("bbb")のどちらか一方だけUDAを持ってるシンボルを取り出したい、とか。
こうなるともう、自分でそれ用のテンプレートを作る必要があります。
このとき、ベースはgetSymbolsByUDAの中身を参考にするといいです。これが参考にできた暁には、テンプレートメタマジシャンの称号が獲得できるでしょう。
というか、getSymbolsByUDAの中は相当高度なテンプレートメタ黒魔術が使われているので、D言語の暗黒面を垣間見たって気分になりますね。
isAccessibleMember
でprivateとかアクセスできないのをはじくため、コンパイルできるかどうか確認してみたりなんだり……この挙動の仔細について記すにはこの余白は(ry
ちなみに、この記事の書き始めは上記のようなモノでしたが、今はちょっと変わっています
UDAで取り出したシンボルを活用する
実際のところ、UDAでシンボルを取り出して何をするの?というのが問題です。
今のところ私が観測したとこによると、シリアライザ/デシリアライザの対象を絞り込んだり、カスタマイズするのにつかわれるケースが多いようです。
-
msgpack-d1では、
@nonPacked
というUDAを使って、シリアライズしないメンバを指定しています。 -
asdf2では
@serializationIgnore
で無視したり、@serializedAs
でシリアライザのハンドラを外部から指定したりしています。 -
cerealed3では
@NoCereal
で無視、@Bits!1
で配置するビット数の指定などができます。
ここでは、例として以下のように構造体・クラスをUDAを使用してフォーマットする関数を考えてみましょう。
- クラス・構造体を引数に取る
formatData
というテンプレート関数を定義する -
formatData
はメンバ変数に文字列のUDAがあったら、そのUDAをformatの第一引数にして、内容を文字列化する -
formatData
は文字列のUDAがついていない変数は文字列化しない
さて、hasUDA, getUDAs, getSymbolsByUDA どのへんが使えるでしょう。
今回は文字列化の対象がUDAがついているものに限られるので、getSymbolsByUDAを使用してメンバのシンボルを列挙しましょう。
列挙されたものはstatic foreachを使って走査的に処理し、getUDAsを用いてUDAを取り出します。
インスタンスのメンバにアクセスするのはちょっと手間が必要で、たとえば変数hogehogeのUDA付きメンバがstatic foreachでsymbol
にaliasされる場合、__traits(getMember, hogehoge, __traits(identifier, symbol))
としてアクセスします。(何とかならないのかこれ。)
import std;
struct Data
{
int foo;
string bar;
@("%.6f") real x;
@("%.3f") float y;
@("%06X") uint z;
}
string formatData(T)(T data)
{
string result;
static foreach (symbol; getSymbolsByUDA!(T, string)) {
result ~= format(getUDAs!(symbol, string)[0],
__traits(getMember, data, __traits(identifier, symbol)))
~ "\n";
}
return result;
}
void main()
{
Data data;
with (data) {
foo = 1;
bar = "hoge";
x = 10;
y = 20;
z = 30;
}
writeln(formatData(data));
}
UDAで取り出したシンボルを活用する - 応用編
もう少し高度な例はこちら
- クラス・構造体を引数に取る
formatData
というテンプレート関数を定義する -
formatData
は、引数のメンバ変数に@formattedWith!"文字列"
というテンプレートのインスタンスで指定されたUDAがあったら、そのUDAをformatの第一引数にして、内容を文字列化する -
formatData
は、引数のメンバ変数に@formattedBy!関数
というテンプレートのインスタンスで指定されたUDAがあれば、そのUDAのインスタンス化に使用した関数に変数を渡して文字列を得る。 -
formatData
は、引数のメンバ変数がクラス・構造体なら再帰的に文字列化する -
formatData
は、上記のUDAがなければ、std.conv.text(member)を使って文字列化する。
とりあえずすぐにわかる問題として、UDAがついていないメンバも出力するため、getSymbolsByUDAは使用不可、というのがありますね。あるんです。__traits(allMembers, T)とかしないといけないので、getSymbolsByUDA参考に新しいテンプレートを作りましょう……
ハードル高くないですか?高いですよね。私も高いと思います。私思うに、このへんPhobosもう少し頑張るべきでは。
さて、このような場合に定石となるのは、以下の3つのテンプレートを作成することです。この定石はasdfで使われています(というか、asdfで使われているものをここでは定石として紹介します)。
-
isXXXX
: UDAが特定のパターンにマッチするかどうかを確認します。isFormattedWith
なら、formattedWith
が文字列でインスタンス化されていることを検査したり、isFormattedBy
なら、formattedBy
が文字列を返す関数でインスタンス化されているかってところを検査したりします。 -
hasXXXX
: シンボルが特定のUDAを持つかどうかを判定します。hasFormattedWith
なら、isFormattedWith
にマッチするUDAを持っているかを確認します。 -
getXXXX
: シンボルやUDAから使いたい情報を取り出します。getFormattedWith
であれば、formattedWith
をインスタンス化した文字列を取り出します。
例として、formattedWith
に関する上記3つのテンプレートが以下になります。
import std;
struct formattedWith(string str) {}
enum bool isFormattedWith(alias uda) = isInstanceOf!(formattedWith, uda);
enum bool hasFormattedWith(alias symbol) = Filter!(isFormattedWith, __traits(getAttributes, symbol)).length > 0;
template getFormattedWith(alias value)
{
static if (isFormattedWith!value)
{
// UDAから文字列を取り出す
enum string getFormattedWith = TemplateArgsOf!value[0];
}
else
{
// シンボルからUDAを取り出す
alias uda = Filter!(isFormattedWith, __traits(getAttributes, value))[0];
// UDAから文字列を取り出す
enum string getFormattedWith = TemplateArgsOf!uda[0];
}
}
struct Data
{
int foo;
string bar;
@formattedWith!"%.6f" real x;
@formattedWith!"%.3f" float y;
@formattedWith!"%06X" uint z;
}
static assert(isFormattedWith!(formattedWith!"test"));
static assert(getFormattedWith!(formattedWith!"test") == "test");
static assert(!hasFormattedWith!(Data.foo));
static assert( hasFormattedWith!(Data.x));
static assert(getFormattedWith!(Data.x) == "%.6f");
これらを使うには、以下に挙げる例のようにします。基本的には __traits(allMembers, T)
ですべてのメンバに対して処置し、 各メンバへのアクセスは __traits(getMember, data, memberName)
で行います(プライベートのメンバも取得できるがモジュール外からのアクセスはできない)。
あるいは tupleof
(プライベートのメンバも取得できるし、モジュール外からのアクセスもできる)でもOKです。用途によって使い分けます。
以下では使用していませんが、プライベートかどうかは、 __traits(getProtection, xxx)
でも取得可能ですので、問題になったら使用を検討しましょう。
is(typeof(xxx))
や__traits(compiles, xxx)
で確認してもいいですが(たぶんコンパイル時間で多少不利になります)。
例は以下。
string formatData(T)(T data)
{
string result;
static foreach (memberName; __traits(allMembers, T)) {
static if (hasFormattedWith!(__traits(getMember, data, memberName))) {
// formattedWithで変換用の書式があれば
result ~= format(getFormattedWith!(__traits(getMember, data, memberName)),
__traits(getMember, data, memberName))
~ "\n";
}
}
return result;
}
あとは単純にUDAのパターンを増やすだけですね。
最初に挙げた仕様を全部対応させると以下のようになります。
import std;
// formattedWithに関する定義。
struct formattedWith(string str) {}
// isXXXX
enum bool isFormattedWith(alias uda) = isInstanceOf!(formattedWith, uda);
// hasXXXX
enum bool hasFormattedWith(alias symbol) = Filter!(isFormattedWith, __traits(getAttributes, symbol)).length > 0;
// getXXXX
template getFormattedWith(alias value)
{
static if (isFormattedWith!value)
{
// UDAから文字列を取り出す
enum string getFormattedWith = TemplateArgsOf!value[0];
}
else
{
// シンボルからUDAを取り出す
alias uda = Filter!(isFormattedWith, __traits(getAttributes, value))[0];
// UDAから文字列を取り出す
enum string getFormattedWith = TemplateArgsOf!uda[0];
}
}
// formattedByに関する定義。
struct formattedBy(alias func) {}
// isXXXX
enum bool isFormattedBy(alias uda) = isInstanceOf!(formattedBy, uda);
// hasXXXX
enum bool hasFormattedBy(alias symbol) = Filter!(isFormattedBy, __traits(getAttributes, symbol)).length > 0;
// getXXXX
template getFormattedBy(alias value)
{
static if (isFormattedBy!value)
{
// UDAから文字列を取り出す
alias getFormattedBy = TemplateArgsOf!value[0];
}
else
{
// シンボルからUDAを取り出す
alias uda = Filter!(isFormattedBy, __traits(getAttributes, value))[0];
// UDAから文字列を取り出す
alias getFormattedBy = TemplateArgsOf!uda[0];
}
}
struct Hoge
{
int fuga;
string piyo;
}
struct Data
{
static string conv(uint v) { return format("%06d", v); }
int foo;
string bar;
// 以下のように今回関係のない@(42)は無視されます
@(42) @formattedWith!"%.6f" real x;
@formattedWith!"%.3f" float y;
@formattedBy!conv uint z;
Hoge hoge;
}
static assert(isFormattedWith!(formattedWith!"test"));
static assert(getFormattedWith!(formattedWith!"test") == "test");
static assert(!hasFormattedWith!(Data.foo));
static assert( hasFormattedWith!(Data.x));
static assert(getFormattedWith!(Data.x) == "%.6f");
static assert(isFormattedBy!(formattedBy!(Data.conv)));
static assert(getFormattedBy!(formattedBy!(Data.conv))(10) == "000010");
static assert(!hasFormattedBy!(Data.foo));
static assert( hasFormattedBy!(Data.z));
static assert(getFormattedBy!(Data.z)(100) == "000100");
string formatData(T)(T data)
{
string result;
static foreach (memberName; __traits(allMembers, T)) {
static if (hasFormattedWith!(__traits(getMember, data, memberName))) {
// formattedWithで変換用の書式があれば
result ~= format(getFormattedWith!(__traits(getMember, data, memberName)),
__traits(getMember, data, memberName))
~ "\n";
}
else static if (hasFormattedBy!(__traits(getMember, data, memberName))) {
// formattedByで指定された関数を使って変換するなら
result ~= getFormattedBy!(__traits(getMember, data, memberName))(
__traits(getMember, data, memberName))
~ "\n";
}
else static if (is(typeof(__traits(getMember, data, memberName)) == struct)
|| is(typeof(__traits(getMember, data, memberName)) == class)) {
// 構造体やクラスなら
result ~= formatData(__traits(getMember, data, memberName));
}
else static if (is(typeof(text(__traits(getMember, data, memberName))))) {
// textで変換可能なら
result ~= text(__traits(getMember, data, memberName)) ~ "\n";
}
}
return result;
}
void main()
{
Data data;
with (data) {
foo = 1;
bar = "HOGE";
x = 10;
y = 20;
z = 30;
hoge.fuga = 1000;
hoge.piyo = "piyopiyo";
}
writeln(formatData(data));
}