物凄いdelegateがものすごくすごい話とか言っておいて、あまり目新しくなくて申し訳ないのですが、たしか去年だったと思います。
デリゲートにはconst/shared/const shared/immutable属性が付けられるようになりました。
↑物凄いデリゲートのものすごく素晴らしい変化です。
去年はうっかり忘れていましたが去年のAdvent Calendarでこそ特筆すべき重要な変化でした。
何がすごい変化かというと、これに伴い、void delegate() shared
という型が新しく作れるようになり、つまり、 hasUnsharedAliasing
で false
となる デリゲート型が作れるようになったのです。
これだけではピンと来ないかもしれませんが、デリゲートにとってこの特性は、次の点において極めて重要なものです。
-
std.concurrency.spawn
の引数に指定できる -
std.parallelism.task
の引数にできる
すなわち、それまでクラスや構造体のメンバ関数はこれらspawn
やtask
には指定できなかったのですが、この変更によってスレッドを超えて実行することができるようになったのです。
さて、spawn
やtask
に渡せるようになったメンバ関数ですが、これを使いこなすには聞くも涙語るも涙の語るに語れぬ苦労をするわけです。…が、それはまた別のお話として、今回はデリゲートに属性をつけるにはどうしたらいいのか、属性のついたデリゲートはどんな特徴を持つのかについてお話します。
delegateに属性をつけるにはどうしたらいいのか
簡単です。
const
/ shared
/ const shared
/ immutable
メンバ関数をdelegateにすればいいです。
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しましょう。
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つは関数のポインタ、もうひとつはオブジェクトへのポインタまたはフレームポインタ。
実のところ、クラスのメンバ関数は次のような呼び出しと同等のことを行っています。
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();
}
これは
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);
}
これとだいたい同じです。
ですので、
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))
と同じなのです。
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プロパティに属性がつくようなものなので、
class A{}
struct Delgate
{
const(A) ptr;
void function(const(A)) funcptr;
}
みたいな構造体を考えるようなものなのだけど…。
…あれ、これって…。
void main()
{
Delgate dg1;
Delgate dg2;
dg2 = dg1;
}
$ dmd -run main
main.d(11): Error: cannot modify struct dg2 Delgate with immutable members
ぉ、おぅ。