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