www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Outer class reference oddity?

reply Manu <turkeyman gmail.com> writes:
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?
Aug 20
next sibling parent Quirin Schroll <qs.il.paperinik gmail.com> writes:
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:
 ```d
 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.
Aug 20
prev sibling next sibling parent reply Paul Backus <snarwin gmail.com> writes:
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
Aug 20
parent Arafel <er.krali gmail.com> writes:
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.
Aug 20
prev sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
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.
Aug 21
parent reply Manu <turkeyman gmail.com> writes:
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.
Aug 21
parent reply Walter Bright <newshound2 digitalmars.com> writes:
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.
Aug 22
next sibling parent reply Manu <turkeyman gmail.com> writes:
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.

Aug 22
parent reply Walter Bright <newshound2 digitalmars.com> writes:
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.
Aug 23
parent reply Manu <turkeyman gmail.com> writes:
On Sat, 24 Aug 2024 at 13:36, Walter Bright via Digitalmars-d <
digitalmars-d puremagic.com> wrote:

 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.
I logged a bug... and then after I did, I realised that I had already logged one before; so you've got 2 now! That should be twice the motivation to fix it ;) I have moved on... but the reason I bring it up is because it's a cool advertised feature, and for the first time in my life, I had a situation where it seemed immensely useful!
Aug 24
parent reply Walter Bright <newshound2 digitalmars.com> writes:
Thank you.
Aug 25
parent Nicholas Wilson <iamthewilsonator hotmail.com> writes:
On Monday, 26 August 2024 at 06:23:40 UTC, Walter Bright wrote:
 Thank you.
for reference: https://issues.dlang.org/show_bug.cgi?id=24716 https://issues.dlang.org/show_bug.cgi?id=24712 https://github.com/dlang/dmd/pull/16810 , but I don't think I'm doing the right approach. Note that there is a workaround of using `this.outer.value2`. Neither `this.value2` nor `outer.value2` work.
Aug 26
prev sibling parent An <d home.com> writes:
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
Aug 23