Jump to page: 1 2
Thread overview
Outer class reference oddity?
Aug 20
Manu
Aug 20
Arafel
Aug 22
Manu
Aug 23
Manu
Aug 24
Manu
Aug 23
An
August 20
Another thing I've never tried to use in D before; an outer class reference! As usual, I try to use a thing and it doesn't work...

Here's a situation:

import std.stdio;

class Outer1 {
    int value1 = 100;

    class Inner1 {
        void print() {
            writeln("value1 from Outer1: ", value1);
        }
    }

    Inner1 make() { return new Inner1; }
}

class Outer2 : Outer1 {
    int value2 = 200;

    class Inner2 : Outer1.Inner1 {
        override void print() {
            writeln("value1 from Outer1: ", value1); // <- no problem!
            writeln("value2 from Outer2: ", value2); // error : accessing
non-static variable `value2` requires an instance of `Outer2`
        }
    }

    override Inner2 make() { return new Inner2; }
}


So, I define a base class which has an inner class, and then users derive from the base class, but may also derive an inner, but in that arrangement, outer references stop working!

The idea is that outer class is a kind of plugin module, where people can define the global working state for their module, and the inner class is an instance that the application creates many of. Each instance should have access to its respective global working state, and the outer pointer seemed perfect for this... but it seems that from Inner2, it is only possible to access Outer1's scope! It looks like the up-pointer is typed incorrectly; it is typed as the base type, and not as the derived type which was assigned on creation.

The new statement that creates Inner2 is in an override make() function, so it's definitely called from within Outer2's scope, and so can be sure the up-pointer given is for its parent Outer2... so why isn't the derived inner's up-pointer typed as the derived outer type?

As an experiment, I tried to new an Inner2 from Outer1's scope, so that it would attempt to be created given a base context rather than the appropriate derived context, but it rejected the call appropriately:

class Outer1 {
    ....
    Outer2.Inner2 makederived() { return new Outer2.Inner2; }
}

error : cannot construct nested class `Inner2` because no implicit `this` reference to outer class `Outer2` is available


So, as I see it, there's no obvious reason for the up-pointer in the derived inner to not be of the derived outer's type...

So, is there a bug? Or is there some design issue here that can be exploited to cause a failure somehow?


August 20

On Tuesday, 20 August 2024 at 08:48:33 UTC, Manu wrote:

>

Another thing I've never tried to use in D before; an outer class reference! As usual, I try to use a thing and it doesn't work...

Here's a situation:

import std.stdio;

class Outer1 {
    int value1 = 100;

    class Inner1 {
        void print() {
            writeln("value1 from Outer1: ", value1);
        }
    }

    Inner1 make() { return new Inner1; }
}

class Outer2 : Outer1 {
    int value2 = 200;

    class Inner2 : Outer1.Inner1 {
        override void print() {
            writeln("value1 from Outer1: ", value1); // <- no problem!
            writeln("value2 from Outer2: ", value2); // error : accessing non-static variable `value2` requires an instance of `Outer2`
        }
    }

    override Inner2 make() { return new Inner2; }
}

[…]
So, is there a bug? Or is there some design issue here that can be exploited to cause a failure somehow?

This looks like an oversight and a bug to me.

August 20
On Tuesday, 20 August 2024 at 08:48:33 UTC, Manu wrote:
> Another thing I've never tried to use in D before; an outer class reference! As usual, I try to use a thing and it doesn't work...
>
> Here's a situation:
>
> import std.stdio;
>
> class Outer1 {
>     int value1 = 100;
>
>     class Inner1 {
>         void print() {
>             writeln("value1 from Outer1: ", value1);
>         }
>     }
>
>     Inner1 make() { return new Inner1; }
> }
>
> class Outer2 : Outer1 {
>     int value2 = 200;
>
>     class Inner2 : Outer1.Inner1 {
>         override void print() {
>             writeln("value1 from Outer1: ", value1); // <- no problem!
>             writeln("value2 from Outer2: ", value2); // error : accessing
> non-static variable `value2` requires an instance of `Outer2`
>         }
>     }
>
>     override Inner2 make() { return new Inner2; }
> }

It works if I change the erroring line to access value2 through the outer pointer [1] explicitly:

    writeln("value2 from Outer2: ", this.outer.value2); // ok

So yes, this is definitely a bug.

[1]: https://dlang.org/spec/class.html#outer-property
August 20
On 20/8/24 19:11, Paul Backus wrote:

> 
> It works if I change the erroring line to access value2 through the outer pointer [1] explicitly:
> 
>      writeln("value2 from Outer2: ", this.outer.value2); // ok
> 
> So yes, this is definitely a bug.
> 
> [1]: https://dlang.org/spec/class.html#outer-property

Perhaps related? https://issues.dlang.org/show_bug.cgi?id=16215

I remember having hit this issue, I certainly have instances of "this.outer" in my code.
August 21
It doesn't work because you are trying to create a multiple inheritance lookup situation, and that is something D avoids.

That's why qualifying it with `this` works.

May I recommend using abstract interfaces instead - they are simpler, easier to understand, and work.
August 22
On Thu, 22 Aug 2024 at 06:25, Walter Bright via Digitalmars-d < digitalmars-d@puremagic.com> wrote:

> It doesn't work because you are trying to create a multiple inheritance
> lookup
> situation, and that is something D avoids.
>
> That's why qualifying it with `this` works.
>
> May I recommend using abstract interfaces instead - they are simpler,
> easier to
> understand, and work.
>

I don't understand what you mean, or maybe you didn't understand my example?

There's definitely no "multiple inheritance lookup" going on here. There is
only one outer pointer... and it's the same value regardless (the same
outer class instance).
The issue is that it has the wrong type...

A derived inner class is only possible in the context of a derived outer,
no? Because an inner class must be initialised within the calling scope of
its outer... so an inner is initialised by its outer and received a ref to
the outer.
A derived inner can't exist unless either 1: it's derived within the
context if the SAME outer, or 2, it's derived within the context of a
DERIVED outer. Either way, the base-outer is the same base outer... to
super will always have a valid outer.
In the context that a derived inner if defined within a derived outer, then
the outer pointer can be re-typed to the derived outer within the context
of the derived inner.
>From the super, it still sees the outer pointer types as base outer, but
the derived outer knows that the outer pointer is the derived outer, so it can be typed appropriately.

There's only one outer pointer here... it's just typed wrong from the derived inner's perspective.

This has nothing to do with multiple inheritance... I'm pretty sure this is just a bug.


I'm not sure your explanation why qualifying with `this` works is correct... the fact is, it does work, but I have a different theory. Since the outer scope is a kind of special sauce, and the super has a reference to the outer scope which is typed for the base outer, that's in the implicit namespace... the derived type comes along, and it has its own reference to the outer, which is typed correctly for the derived outer, and so `this.outer` works, because it has the correct type... but if you omit the explicit scope and fallback to the local namespace, I reckon somehow the super's outer is taking precedence over the more local outer reference, and that's why the outer appears to be typed wrong.

Like you say, this is all internal compiler magic; there's only actually one outer context pointer, and how the compiler types that and incorporates it into the local namespace is kinda magic... I think there's just a bug.


August 22
On 8/21/2024 9:33 PM, Manu wrote:
> I don't understand what you mean, or maybe you didn't understand my example?

Quite possibly, because your example hurts my head :-/


> There's only one outer pointer here... it's just typed wrong from the derived inner's perspective.

There are two outer pointers. One to Outer2:Outer1, and the other to Outer1.Inner1.Outer1. Multiple inheritance comes into play when the order of lookup is not clear and what to do if the identifier being looked up is found via multiple paths (although in this case is isn't in multiple paths).

A similar thing happened with alias this. When I approved it I did not realize it was multiple inheritance. The problem eventually showed up, and there are numerous bug reports about it behaving in baffling ways. Nobody could figure out a solution to it that made sense, and the result is we cannot remove alias this, but we cannot fix it either, so it is what it is.

Generally speaking, when there are multiple lookup paths, the inevitable result is confusion.

I spent a lot of time working out how nested classes and nested functions work. I'm a bit afraid to start messing with the semantics of it at this point.

A handful of years ago, some contributors thought that having only one "context" pointer for nested functions was too restrictive, and added the ability for a second. After it was incorporated, simple cases worked as expected. But people always try more complex cases, it turned into an unresolvable disaster, and was eventually removed. (Just like alias this, except we're stuck with alias this, as too much code depends on it.)

I strongly recommend abandoning this design pattern.

P.S: It's not about types, it's about the order in which things are searched for.


August 23
On Fri, 23 Aug 2024, 04:41 Walter Bright via Digitalmars-d, < digitalmars-d@puremagic.com> wrote:

> On 8/21/2024 9:33 PM, Manu wrote:
> > I don't understand what you mean, or maybe you didn't understand my
> example?
>
> Quite possibly, because your example hurts my head :-/
>
>
> > There's only one outer pointer here... it's just typed wrong from the
> derived
> > inner's perspective.
>
> There are two outer pointers. One to Outer2:Outer1, and the other to
> Outer1.Inner1.Outer1. Multiple inheritance comes into play when the order
> of
> lookup is not clear and what to do if the identifier being looked up is
> found
> via multiple paths (although in this case is isn't in multiple paths).
>
> A similar thing happened with alias this. When I approved it I did not
> realize
> it was multiple inheritance. The problem eventually showed up, and there
> are
> numerous bug reports about it behaving in baffling ways. Nobody could
> figure out
> a solution to it that made sense, and the result is we cannot remove alias
> this,
> but we cannot fix it either, so it is what it is.
>
> Generally speaking, when there are multiple lookup paths, the inevitable
> result
> is confusion.
>
> I spent a lot of time working out how nested classes and nested functions
> work.
> I'm a bit afraid to start messing with the semantics of it at this point.
>
> A handful of years ago, some contributors thought that having only one
> "context"
> pointer for nested functions was too restrictive, and added the ability
> for a
> second. After it was incorporated, simple cases worked as expected. But
> people
> always try more complex cases, it turned into an unresolvable disaster,
> and was
> eventually removed. (Just like alias this, except we're stuck with alias
> this,
> as too much code depends on it.)
>
> I strongly recommend abandoning this design pattern.
>
> P.S: It's not about types, it's about the order in which things are searched for.
>

I'm not keen to abandon it; I can't imagine any conceivable use for an outer class reference if not exactly this arrangement I show here. This should work, or just delete the feature.

If you can't derive an inner class, then why an inner class at all? Classes exist to be derived, no other reason to use a class and a million reasons against.

I don't see it the way you describe though, you seem to imagine 2 pointers, where I only see one. There's one outer pointer in the base class, simple as that. The only trouble is that it's typed for the base, when in the derived inner it could be re-typed for it's outer.

A simple solution might be that the compiler emit references to outer as (cast(this.super.typeof)outer). That will always work. If the compiler synthesizes that cast when emitting any outer reference, the whole thing will work neatly; there's not 2 pointers, just a bad type.

Alternatively, make this.outer appropriately shadow super.outer? But from an implementation perspective, I'd be less inclined, because that intonates that there's 2 outer references (2 paths, as you say), when there's not.

It's easy to prove that there's not ACTUALLY 2 outer references, check the size of Inner1 and Inner2, they are the same size. Inner 2 is not one pointer larger than Inner1.

>


August 23
On Thursday, 22 August 2024 at 18:39:09 UTC, Walter Bright wrote:
>
> Generally speaking, when there are multiple lookup paths, the inevitable result is confusion.
>
> I spent a lot of time working out how nested classes and nested functions work. I'm a bit afraid to start messing with the semantics of it at this point.
>

Lookup should perform from nearest to farthest

for class hierarchy
from inside its self
from its' derived class
from its' outer class
from its' outer derived class

for function
from inside its self
from its' outer function
if it is a aggregate function -> use above class hierarchy rule
August 23
The reason the inner class feature exists is to support translation from Java code which used inner classes. (It doesn't exist in C++.)

Ok, so there is only one outer. This illustrates how confusing this example is (at least to me!)


> it is typed as the base type, and not as the derived type which was assigned on creation.

It is indeed typed as the static type, not the dynamic type.

I encourage you to submit it to bugzilla.

I suggest just adding the Outer2 qualification, and move on. Or use an alternate method such as interfaces or aggregation.
« First   ‹ Prev
1 2