Help us understand the problem. What is going on with this article?

デリゲートの属性について

More than 5 years have passed since last update.

物凄いdelegateがものすごくすごい話とか言っておいて、あまり目新しくなくて申し訳ないのですが、たしか去年だったと思います。

デリゲートにはconst/shared/const shared/immutable属性が付けられるようになりました。

↑物凄いデリゲートのものすごく素晴らしい変化です。

去年はうっかり忘れていましたが去年のAdvent Calendarでこそ特筆すべき重要な変化でした。
何がすごい変化かというと、これに伴い、void delegate() shared という型が新しく作れるようになり、つまり、 hasUnsharedAliasingfalse となる デリゲート型が作れるようになったのです。
これだけではピンと来ないかもしれませんが、デリゲートにとってこの特性は、次の点において極めて重要なものです。

  1. std.concurrency.spawn の引数に指定できる
  2. std.parallelism.task の引数にできる

すなわち、それまでクラスや構造体のメンバ関数はこれらspawntaskには指定できなかったのですが、この変更によってスレッドを超えて実行することができるようになったのです。

さて、spawntaskに渡せるようになったメンバ関数ですが、これを使いこなすには聞くも涙語るも涙の語るに語れぬ苦労をするわけです。…が、それはまた別のお話として、今回はデリゲートに属性をつけるにはどうしたらいいのか、属性のついたデリゲートはどんな特徴を持つのかについてお話します。

delegateに属性をつけるにはどうしたらいいのか

簡単です。
const / shared / const shared / immutable メンバ関数をdelegateにすればいいです。

main.d
class A { void foo() const { } } 
void main()
{
    auto a = new A;
    auto dg = &a.foo;
    pragma(msg, typeof(dg));
}
$ dmd -run main
void delegate() const

クロージャでやりたいのだけど

castしましょう。

main.d
import std.stdio;

void main()
{
    // 以下の書き方はNGです。
    // 個人的にはこんな風にかけるといいなぁと思います。
//  auto constdg = delegate void() const { writeln("hello 2"); };

    // しょうがないのでconstな挙動になるように"自分で気をつけて"
    // 以下の様な無理矢理なキャストをかませます。
    // const性は自分で保たなければならないので気をつけましょう。
    // 大切なことなので2回言いました。
    auto constdg = cast(void delegate() const)delegate void() { writeln("hello 2"); };
    pragma(msg, typeof(constdg));
}
$ dmd -run main
void delegate() const

デリゲートに属性をつける意味

デリゲートは、2つのパラメータを持っています。
1つは関数のポインタ、もうひとつはオブジェクトへのポインタまたはフレームポインタ。
実のところ、クラスのメンバ関数は次のような呼び出しと同等のことを行っています。

main.d
import std.stdio;
class A
{
    string _name;

    void printName() const
    {
        writeln(_name);
    }
    void setName(string name)
    {
        _name = name;
    }
}

void main()
{
    auto a = new A;

    a.setName("foo");
    a.printName();
}

これは

main.d
import std.stdio;
class A{ string _name; }

void printName(const A this_)
{
    writeln(this_._name);
}
void setName(string name, A this_)
{
    this_._name = name;
}
void main()
{
    auto a = new A;

    setName("foo", a);
    printName(a);
}

これとだいたい同じです。
ですので、

main.d
import std.stdio;

class A
{
    string _name;
    void setName(string name)
    {
        writeln("1: A.setName = ", name);
        _name = name;
    }
}
void setName(string name, A this_)
{
    writeln("2: .setName = ", name);
    this_._name = name;
}
void main()
{
    auto a = new A;

    union DelegateData
    {
        // ABIに従い、デリゲートのメモリ構成をstructで表現します。
        struct { A ptr; void function(string, A) funcptr; }
        void delegate(string) dg;
    }
    DelegateData dgdata;
    dgdata.ptr     = a;
    dgdata.funcptr = &setName;
    auto dg1 = dgdata.dg;
    auto dg2 = &a.setName;

    pragma(msg, typeof(dg1));
    pragma(msg, typeof(dg2));

    dg1("name1");
    dg2("name2");
}

こんなこともできます。
もうちょっと詳しく述べると、これは、D言語の関数の呼び出し規約に仕様として規定されています。
dlang.org:ABI

じつはstd.functional.toDelegateはこういうようなことをして、無理やりfunction型をdelegate型に変換したりします。

さて、上記例でvoid delegate(string)void function(string,A)がだいたい同じということが分かりましたが、
void delegate() constと同じものは何でしょうか?
Aクラスのメンバ関数void printName() constがデリゲートをとるとvoid delegate() const型になることから、もうお分かりですね。
void function(const(A))と同じなのです。

main.d
import std.stdio;

class A
{
    string _name;
    void printName() const
    {
        writeln("A.name = ", _name);
    }
    void setName(string name)
    {
        _name = name;
    }
}
// ABI(呼び出し規約)に従うと、thisポインタは関数引数の一番最後の隠し引数。
void printName(const(A) this_)
{
    writeln("a.name = ", this_._name);
}
// ABI(呼び出し規約)に従うと、thisポインタは関数引数の一番最後の隠し引数。
void setName(string name, A this_)
{
    this_._name = name;
}
void main()
{
    auto a1 = new A;
    auto a2 = new A;

    union DelegateData1
    {
        // ABIに従い、デリゲートのメモリ構成をstructで表現します。
        struct { A ptr; void function(string,A) funcptr; }
        void delegate(string) dg;
    }
    union DelegateData2
    {
        // ABIに従い、デリゲートのメモリ構成をstructで表現します。
        struct { A ptr; void function(const(A)) funcptr; }
        void delegate() const dg;
    }

    DelegateData1 dgdata1;
    dgdata1.ptr     = a1;
    dgdata1.funcptr = &setName;
    DelegateData2 dgdata2;
    dgdata2.ptr     = a1;
    dgdata2.funcptr = &printName;
    auto dg1 = dgdata1.dg;
    auto dg2 = &a2.setName;
    auto dg3 = dgdata2.dg;
    auto dg4 = &a2.printName;

    pragma(msg, typeof(dg1));
    pragma(msg, typeof(dg2));
    pragma(msg, typeof(dg3));
    pragma(msg, typeof(dg4));

    dg1("name1");
    dg2("name2");
    dg3();
    dg4();
}

前置きが長くなりましたが、この通り、delegateのconst/shared/const shared/immutable属性の付与は、クラスのメンバ関数でthisの型にconst/shared/const shared/immutable属性を付与したのと同じ効果があるのです。

もうちょっと考察

通常メンバ関数から取得したデリゲートは、ptrプロパティにthisポインタを持ちますが、属性のないデリゲートはptr(this)の型に属性がないようなものなので、例えばconst(A)な型のインスタンスからはvoid delegate() constしか取り出すことはできません。
逆に、属性のあるデリゲートを使えば、属性のあるthisポインタを通じて属性のあるメンバ関数を呼び出すのと同じ意味を持ちます。
これにより、デリゲートの属性を調べることで、冒頭の事例のようにそこからアクセスされるものがshared性を保つか(!hasUnsharedAliasingな特性)どうかなどを調べることが可能となっています。

さらに深入り。そして禁断の領域へ…

ちなみに、メンバ関数でthisの型に属性がつくということは、delegateのptrプロパティに属性がつくようなものなので、

main.d
class A{}
struct Delgate
{
    const(A) ptr;
    void function(const(A)) funcptr;
}

みたいな構造体を考えるようなものなのだけど…。
…あれ、これって…。

main.d
void main()
{
    Delgate dg1;
    Delgate dg2;
    dg2 = dg1;
}
$ dmd -run main
main.d(11): Error: cannot modify struct dg2 Delgate with immutable members

ぉ、おぅ。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away