Abstract classes vs interfaces, casting from void*
Simen Kjærås
simen.kjaras at gmail.com
Fri Aug 9 13:39:53 UTC 2019
On Friday, 9 August 2019 at 12:26:59 UTC, John Colvin wrote:
> import std.stdio;
>
> interface I
> {
> void foo();
> }
>
> class C : I
> {
> override void foo() { writeln("hi"); }
> }
>
> abstract class AC
> {
> void foo();
> }
>
> class D : AC
> {
> override void foo() { writeln("hi"); }
> }
>
> void main()
> {
> auto c = new C();
> writeln(0);
> (cast(I)cast(void*)c).foo();
> writeln(1);
> (cast(C)cast(void*)c).foo();
> writeln(2);
> (cast(I)cast(C)cast(void*)c).foo();
>
> auto d = new D();
> writeln(3);
> (cast(AC)cast(void*)d).foo();
> writeln(4);
> (cast(D)cast(void*)d).foo();
> writeln(5);
> (cast(AC)cast(D)cast(void*)d).foo();
> }
>
> This produces the output:
>
> 0
> 1
> hi
> 2
> hi
> 3
> hi
> 4
> hi
> 5
> hi
>
> Why is there no "hi" between 0 and 1?
We're getting into somewhat advanced topics now. This is
described in the Application Binary Interface page of the
documentation[0]. In short: classes and interfaces both use a
vtable[1] that holds pointers to each of their methods. When we
cast a class instance to an interface, the pointer is adjusted,
such that the interface's vtable is the first member. Casting via
`void*` bypasses this adjustment.
Using `__traits(classInstanceSize)`, we can see that `C` has a
size of 12 bytes, while `D` only is 8 bytes (24 and 16 on
64-bit). This corresponds to the extra interface vtable as
described above.
When we first cast to `void*`, no adjustment happens, because
we're not casting to an interface. When we later cast the `void*`
to an interface, again no adjustment happens - in this case
because the compiler doesn't know what we're casting from.
If we use `__traits(allMembers, C)`, we can figure out which
methods it actually has, and implement those with some extra
debug facilities (printf):
class C : I
{
override void foo() { writeln("hi"); }
override string toString() { writeln("toString");
return ""; }
override hash_t toHash() { debug printf("toHash");
return 0; }
override int opCmp(Object o) { writeln("opCmp"); return
0; }
override bool opEquals(Object o) { writeln("opEquals");
return false; }
}
If we substitute the above in your program, we see that the
`toString` method is the one being called. This is simply because
it's at the same location in the vtable as `foo` is in `I`'s
vtable.
When casting from a class to a superclass, no pointer adjustment
is needed, as the vtable location is the same for both.
We can look closer at the vtable, and see that for a new
subclass, additional entries are simply appended at the end:
class C {
void foo() {}
}
class D : C {
void bar() {}
}
unittest {
import std.stdio;
C c = new C();
D d = new D();
writeln("Pointer to foo(): ", (&c.foo).funcptr);
writeln("Pointer to bar(): ", (&d.bar).funcptr);
writeln("Pointer to foo() in C's vtable: ", c.__vptr[5]);
writeln("Pointer to foo() in D's vtable: ", d.__vptr[5]);
writeln("Pointer to bar() in D's vtable: ", d.__vptr[6]);
}
As we see, `foo()` has the position in the vtable for both `c`
and `d`, while `D`'s new `bar()` method is added as the next
entry.
--
Simen
[0]: https://dlang.org/spec/abi.html
[1]: https://en.wikipedia.org/wiki/Virtual_method_table
More information about the Digitalmars-d-learn
mailing list