Jump to page: 1 2 3
Thread overview
Abstract classes vs interfaces, casting from void*
Aug 09, 2019
John Colvin
Aug 09, 2019
kinke
Aug 09, 2019
John Colvin
Aug 09, 2019
Simen Kjærås
Aug 10, 2019
John Colvin
Aug 10, 2019
John Colvin
Aug 10, 2019
Antonio Corbi
Aug 10, 2019
John Colvin
Aug 10, 2019
Alex
Aug 10, 2019
John Colvin
Aug 10, 2019
Alex
Aug 11, 2019
John Colvin
Aug 11, 2019
Alex
Aug 11, 2019
John Colvin
Aug 11, 2019
Alex
Aug 11, 2019
John Colvin
Aug 11, 2019
Adam D. Ruppe
Aug 11, 2019
Jonathan M Davis
Aug 11, 2019
Alex
Aug 10, 2019
Timon Gehr
Aug 11, 2019
John Colvin
Aug 14, 2019
wjoe
August 09, 2019
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?
August 09, 2019
On Friday, 9 August 2019 at 12:26:59 UTC, John Colvin wrote:
> Why is there no "hi" between 0 and 1?

Because you are treating the unadjusted object pointer as interface pointer and then call the only virtual function of that interface, in the 2nd vtbl slot (after the TypeInfo ptr). Casting a class ref to an interface offsets the pointer, so that the interface ref points to the interface vptr for that object instance. This is missing in that line, and so you are invoking the first virtual function of class C, which is some base function in `Object`.

August 09, 2019
On Friday, 9 August 2019 at 13:19:14 UTC, kinke wrote:
> On Friday, 9 August 2019 at 12:26:59 UTC, John Colvin wrote:
>> Why is there no "hi" between 0 and 1?
>
> Because you are treating the unadjusted object pointer as interface pointer and then call the only virtual function of that interface, in the 2nd vtbl slot (after the TypeInfo ptr). Casting a class ref to an interface offsets the pointer, so that the interface ref points to the interface vptr for that object instance. This is missing in that line, and so you are invoking the first virtual function of class C, which is some base function in `Object`.

Ok, makes sense, thanks.
August 09, 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
August 10, 2019
On Friday, 9 August 2019 at 13:39:53 UTC, Simen Kjærås wrote:
> 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

Thanks for the extra detail.

Is there a solid reason to ever use an interface over an abstract class? (Other than multiple inheritance).

I'm such a noob at anything related to OO.
August 10, 2019
On Friday, 9 August 2019 at 13:39:53 UTC, Simen Kjærås wrote:
> <snip>

Thanks for the extra detail.

Is there a solid reason to ever use an interface over an abstract class? (Other than multiple inheritance).

I'm such a noob at anything related to OO.
August 10, 2019
On Saturday, 10 August 2019 at 08:20:46 UTC, John Colvin wrote:
> On Friday, 9 August 2019 at 13:39:53 UTC, Simen Kjærås wrote:
>> <snip>
>
> Thanks for the extra detail.
>
> Is there a solid reason to ever use an interface over an abstract class? (Other than multiple inheritance).
>
> I'm such a noob at anything related to OO.

Hi John.

One reason could be data. Abstract classes can hold data, interfaces can't.

Antonio
August 10, 2019
On Saturday, 10 August 2019 at 08:20:46 UTC, John Colvin wrote:
> On Friday, 9 August 2019 at 13:39:53 UTC, Simen Kjærås wrote:
>> <snip>
>
> Thanks for the extra detail.
>
> Is there a solid reason to ever use an interface over an abstract class? (Other than multiple inheritance).
>
> I'm such a noob at anything related to OO.

The general question is tricky, as different languages differ in details what is forced and what is allowed for abstract classes and interfaces.

But roughly speaking, my opinion is: if you can/want to provide some default behavior than you are about to write an abstract class.
If you are about to provide information/restriction of behavior, then this is more like an interface.
August 10, 2019
On Saturday, 10 August 2019 at 10:02:02 UTC, Antonio Corbi wrote:
> On Saturday, 10 August 2019 at 08:20:46 UTC, John Colvin wrote:
>> On Friday, 9 August 2019 at 13:39:53 UTC, Simen Kjærås wrote:
>>> <snip>
>>
>> Thanks for the extra detail.
>>
>> Is there a solid reason to ever use an interface over an abstract class? (Other than multiple inheritance).
>>
>> I'm such a noob at anything related to OO.
>
> Hi John.
>
> One reason could be data. Abstract classes can hold data, interfaces can't.
>
> Antonio

That's a reason to use an abstract class, not a reason to use an interface.
August 10, 2019
On Saturday, 10 August 2019 at 10:11:15 UTC, Alex wrote:
> On Saturday, 10 August 2019 at 08:20:46 UTC, John Colvin wrote:
>> On Friday, 9 August 2019 at 13:39:53 UTC, Simen Kjærås wrote:
>>> <snip>
>>
>> Thanks for the extra detail.
>>
>> Is there a solid reason to ever use an interface over an abstract class? (Other than multiple inheritance).
>>
>> I'm such a noob at anything related to OO.
>
> The general question is tricky, as different languages differ in details what is forced and what is allowed for abstract classes and interfaces.
>
> But roughly speaking, my opinion is: if you can/want to provide some default behavior than you are about to write an abstract class.
> If you are about to provide information/restriction of behavior, then this is more like an interface.

Ok. What would go wrong (in D) if I just replaced every interface with an abstract class?
« First   ‹ Prev
1 2 3