[D言語]ある型の全メンバーの属性を取得する


あるクラス/構造体のメンバーに User Defined Attribute(以下UDA) を設定してそれを元にメソッド等を自動生成する、という処理がしたかったためしばらく試行錯誤した結果です。この条件だと動かない、等あればご指摘をお願いします。

ソースコード

struct Attr { string x; }

mixin template GenerateFoobar()
{
    mixin(
    {
        import std.format : format;
        import std.meta : Alias;
        import std.traits : getUDAs, hasUDA;

        alias This = typeof(this);

        string ret;

        foreach (name; __traits(allMembers, This))
        {
            alias field = Alias!(__traits(getMember, This, name));

            static if (hasUDA!(field, Attr))
            {
                ret ~= getUDAs!(field, Attr)[0].x;
            }
        }

        return `enum foobar = "%s";`.format(ret);
    }());
}

unittest
{
    struct Test
    {
        @Attr("foo") int x;
        @Attr("bar") int y;
        mixin GenerateFoobar;
    }

    static assert(Test.foobar == "foobar");
}

解説

mixin template GenerateFoobar()
{
    mixin(...);
}

直接文字列 mixin しないのは、mixin(GenerateFoobar!(typeof(this))); のように typeof(this) を書きたくないからです。もしも文字列 mixin で書くと以下のようになります。

string generateFoobar(T)()
{
    return "some code using T";
}

struct Test
{
    mixin(generateFoobar!(typeof(this))());
}

このように余計な typeof(this) が必要になります(もちろん、コード生成の時に typeof(this) を必要としないのであればこの限りではありません)。逆に typeof(this) 以外を渡したい場合はこのようにすると良いと思います。

また、文字列 mixin するときに関数リテラルを即時呼び出ししていますが、これには明確な意図があります。サンプルとして簡略化したというわけではありません。文字列 mixin の部分を別関数として書くとこのようになります。

foobar.d
module foobar;

mixin template GenerateFoobar()
{
    mixin(generateFoobar!(typeof(this))());
}

private string generateFoobar(T)()
{
    return "some code";
}
test.d
module test;

struct Test
{
    mixin GenerateFoobar;
}

しかしこれはコンパイルできません。実際にエラーの内容を見ればすぐわかりますが、これは test モジュールから generateFoobar 関数が見えないからです(最初に示したコードだと定義箇所と利用箇所が同一モジュールになってしまうため分離しました)。generateFoobar を public にすればエラーは解決しますが、名前の衝突の危険などを考えるとあまりやりたくないところです。

この例で分かる通り、template mixin ではその中身が利用する側のスコープで名前解決がなされます。他にハマりうるポイントとしては、このような書き方をしても動きません(正確には警告がでてコンパイル自体は通ります)。

import std.format : format;

mixin template GenerateFoobar()
{
    mixin(`enum foobar = %s;`.format(42));
}

これも同じく std.format 関数が利用側から見えないことが原因です。もちろん、「利用するときは必ず一緒に std.format を import してください」という条件をつければ警告は消えますが、template mixin の中で import しておくのが無難です(ついでに言えばその利用側で import myformat : format = myformat; とされるとコンパイルすら通らなくなります。format 関数が std.format ではなく myformat.myformat に変わっているためです)。

なお、mixin template の中で import すると利用側にも波及しますので、import する場所には注意が必要です。

mixin template GenerateFoobar()
{
    import std.format : format;
    mixin({
        import std.traits : getUDAs;
        return "some code";
    }());
}

struct Test
{
    mixin GenerateFoobar;
    // format is VISIBLE from here!
    // getUDAs is not visible from here.
}

さて、ようやく本題の UDA です。

foreach (name; __traits(allMembers, This))

まず __traits(allMembers, This) でメンバ名をすべて取得します。allMembersスーパークラスを含めたすべてのメンバーを取得することに注意してください。特に class は必ず Object をスーパークラスとして持っていますので、予想しないメンバーが含まれる可能性があります。これを避けたい場合には __traits(derivedMembers, Type) が使えます。

なお、__traits(allMembers, This) が返すのは、std.meta.AliasSeq です(たぶん)。タプルのようなものですが、メタ、と名につく通り普通のタプルとは少々異なる動きをします。例えばこの部分は
[__traits(allMembers, This)] と書いても動きます。これは配列の [] の中に AliasSeq が展開されるからです。これは std.tuplecons.Tuple では expand に相当します。

それでは、実際に属性の保持を判定し、取得する部分に移ります。

alias field = Alias!(__traits(getMember, This, name)); 

static if (hasUDA!(field, Attr))
{
     ret ~= getUDAs!(field, Attr)[0].x;
}

まず __traits(getMember, This, mame) は、name == "hoge" とすると This.hoge と直接書いたのと同等になります。これをそのまま hasUDA!(__traits(getMember, This, name), Attr) としてもいいのですが、getUDAs でも使うので DRY に反します。そこで alias を使うことになるのですが、alias field = __traits(getMember, This, name); としてはダメです。これを可能にするために、なんでも alias にできる std.meta.Alias を使います。

alias を文で書くときと template の引数として書くときで受け取れるものが違う理由はよくわかっていません。「それが仕様だからです」以外に明快な定義はあるのでしょうか?

hasUDA は見た通りですので説明は省略します。getUDAs ですが、これは先程と同様 AliasSeq を返します。また、UDAs の名が示す通り UDA は複数指定可能ですので、複数指定されると困る場合は static if を使って判定すべきです。

あとはお好きなように CTFE を駆使してコードを生成してください。

D言語とはあまり関係のない TIPS

Qiitaでバッククォート3つを打ったあと d と入力することで d から始まるシンタックス定義がずらっと出てくるが、d を選んでエンターを押すと改行が入ってしまう。これを防ぐには d を打ってすぐ : を打つと良い。元々はファイル名を指定するためのコロンだが、未指定にすることもできる。ついでに言えば d と打って出てくる最初の候補は(投稿した時点では)diff なので、d: と打てば少しだけ速く D言語を指定できる。