classとinterface, abstract classの違いはなんでしょう?
生粋のDの言語erたる皆さんは簡単だとお思いでしょう。ええ、簡単です。以下の違いがあります。
- classとabstract classは単一継承のみ許可されていますが、interfaceは複数継承することができる。
- classとabstract classにはメンバ変数を持たせることができるが、interfaceはできない
- classはすべてのメンバ関数を実装しなければならないが、abstract classとinterfaceはそうでもない
- classとabstract classはObjectを継承していることが確実ですが、interfaceはそうではない
- などなど
以上、と言いたいところですが、もう一つ重要なポイントが有ります
classとabstract classはABIが同じだが、interfaceは異なる
どういうことかと言いますと、次のコードを見てみると明らかになります。
import std.stdio;
interface I { void foo(); }
class A: I {void foo(){} }
abstract class C { void foo(); }
class B: C { override void foo(){} }
void main()
{
auto a = new A;
auto b = new B;
I i = a;
C c = b;
writeln(cast(void*)a);
writeln(cast(void*)i);
writeln(cast(void*)b);
writeln(cast(void*)c);
}
$ dmd -run main
251FD0
251FD8
251FC0
251FC0
classや、abstract class、interfaceは、インスタンス(メモリ上のどこかにある実態)へのアドレスによって参照型を形成します。
そのアドレスを見たいとき、 cast(void*)obj
なんてことをしたりします。
上記例ではclassからinterfaceにアップキャストしたiのアドレスが、aのアドレスと異なっているのに対し、abstract classにアップキャストしたcのアドレスがbのアドレスと一致していることがわかります。
これは、言語仕様にある通り、
クラスオブジェクトのインターフェイスへのキャストは、 オブジェクトのベースアドレスに、インターフェイスのvptrへのオフセットを加算することで行われます。 dlang.org:ABI (訳)
というものから来ているものです。
classとabstract classのそれぞれのABIはクラスオブジェクトのメモリ構成で定義されたものであり、interfaceのABIは、そのなかのvtbl[]のアドレス、ということになります。
つまり、interfaceのアドレスをクラスインスタンスのアドレスとして使うと、タイヘンなことになるということです。
interface I {}
class A: I { void foo(){} }
void main()
{
auto a1 = new A;
auto i = cast(I)a1;
assert(cast(void*)a1 !is cast(void*)i); // a1のアドレスとiのアドレスは別物
auto p = cast(void*)i;
auto a2 = cast(A)p;
assert(a1 !is a2); // 一度純粋なアドレスを経由してしまうと型情報を失うので戻せない
a2.foo(); // ←当然これはNG
}
え?やらないって?
class/interfaceを引数に取るdelegateをcastする場合
本題です。
このABIの違い、キャストのされ方の違いによって、delegateをキャストする場合に問題となるケースが有ります。
import std.stdio;
interface I { void foo(); }
class A: I {void foo(){} }
abstract class C { void foo(); }
class B: C { override void foo(){} }
void main()
{
auto a = new A;
auto b = new B;
void foo(A i) { writeln(cast(void*)i); i.foo(); }
void bar(B c) { writeln(cast(void*)c); c.foo(); }
auto dgI = cast(void delegate(I))&foo;
auto dgC = cast(void delegate(C))&bar;
writeln(cast(void*)a);
foo( a ); // (1)
dgI( a ); // (2)
writeln(cast(void*)b);
bar( b ); // (3)
dgC( b ); // (4)
}
こうするとちょっとだけ難しくなります。このキャスト、オブザーバーとか作る場合にたまーにこんなかんじにしたくなる時があります。
上記コードの(1)~(4)の中で、危険な呼び出しはどれでしょう。
(1). void foo(A i) の関数をA型の変数aを使って呼び出す場合
これは問題ありません。普通の呼び出しです。
(2). void delegate(I) のデリゲートをA型の変数aを使って呼び出す場合
これは問題です。
実引数となる変数aがA型なのに対して、デリゲートの仮引数の型がI型であることから、classからinterfaceへの暗黙のアップキャストが行われます。
すなわち、参照のアドレスがA型のクラスオブジェクトのベースアドレスからvtbl[]のアドレスへと加算されて関数へと渡されます。
しかしながら、delegateの本体はfooであるため、渡されたアドレスが加算されたvtbl[]のアドレスだとは知らずにA型のベースアドレスであるものとして処理します。
(3). void bar(B i) の関数をB型の変数bを使って呼び出す場合
これは問題ありません。普通の呼び出しです。
(4). void delegate(C) のデリゲートをB型の変数bを使って呼び出す場合
これは、interfaceとちがって、問題ありません。
実引数となる変数bがB型なのに対して、デリゲートの仮引数の型がC型であることから、classからabstract classへの暗黙のアップキャストが行われます。
すなわち、関数に渡される参照のアドレスがクラスオブジェクトのベースアドレスから…変わりません。
当然ながら、delegateの本体がbarであっても、渡されたB型のクラスオブジェクトのベースアドレスを、C型を継承したクラスオブジェクトのベースアドレスとして処理することになんの問題もありません。
まとめ
いや、要するにこの前これで10分位ハマったんだよ…。10分で解決できたのは運が良かった…。
- interfaceとabstract classはABIがまるっきり違う。
- そのため、interfaceはアップキャストされると、示すアドレスがObjectの先頭アドレスではなくなる。
- ので、delegateを引数の型を変えるレベルでアグレッシブにキャストするときは、引数として渡されるものの整合性を保つように注意すること。