View mode: basic / threaded / horizontal-split · Log in · Help
July 11, 2012
Re: readonly?
On Tuesday, 10 July 2012 at 19:27:56 UTC, Namespace wrote:
> Maybe D need's a readonly keyword.
> [...]
> Or has D an alternative?

https://github.com/D-Programming-Language/dmd/pull/3
July 12, 2012
Re: readonly?
On Wednesday, July 11, 2012 09:51:37 Ali Çehreli wrote:
> On 07/11/2012 08:52 AM, David Nadlinger wrote:
> > I fail to see anything inconsistent here.
> 
> Most other operations act on the object:
> 
> class B
> {
> // ...
> }
> 
> auto b = new B();
> 
> ++b; // on the object
> b > b; // on the object
> // etc.
> &b; // on the reference
> 
> That can be seen as an inconsistency. Perhaps it is that the
> non-overridable operators are on the class reference?

It's the fact that in the type system B is a reference to the class named B, 
_not_ the class itself. ++b operates on the object itself, because it's not 
legal to increment a reference. Such an operation makes no sense. Almost all 
operations get forwarded to the object just like using . with a pointer gets 
forwarded to the pointee.

One of the few operations which makes sense on the reference itself is &, 
since it gives you the address of the reference. On the other hand, it makes 
no sense to take the address of the object itself, since there is _no way_ in 
the type system to refer to that object, and the type system purposefully 
makes it so that you don't and can't mess with class objects directly, since 
it avoids problems such as object slicing.

So, while it may seem odd at first that & operates on the reference itself 
rather than the object, remember that the fact that a reference uses a pointer 
is an implementation detail which is _not_ represented in the type at all. 
Think of it like this:

struct S
{
//opDispatch defined here and
//all overloaded operators defined here to forward to *ptr...

private:
C* ptr;
}

S s;

&s would naturally refer to s, not ptr, and there's no way to access ptr 
directly. What engenders so much confusion is that in D's type system, S is 
referred to as C, so you essentially get

struct C
{
...

private:
C* ptr;
}

C c;

where C refers to the struct everywhere except with ptr, which refers to the 
actual class object.

So, while some of the behaviors with regards to classes may seem odd at first, 
they're actually _very_ consistent with everything else.

- Jonathan M Davis
July 12, 2012
Re: readonly?
On Wednesday, July 11, 2012 23:09:17 Artur Skawina wrote:
> The advantages of having pointers to classes? Eg solving the problem that
> triggered this thread w/o hacks like ClassPtr (Rebindable is an even worse
> hack). [1]

You'd also lose polymorphism, which you don't with Rebindable. In D, 
references are polymorphic. Pointers are not. You really don't want to be 
using a pointer to a class. D was designed with the idea that classes would 
accessed through references, not pointers, and the related features are all 
designed around that.

- Jonathan M Davis
July 12, 2012
Re: readonly?
On Wednesday, July 11, 2012 10:56:23 Artur Skawina wrote:
> Can anybody think of a reason to keep the current (broken) behavior?

Easily.

Making Object* point to the object itself rather than the reference would be 
so broken that it's not even funny. I can understand why you would think that 
such a change should be made, but that's only because you haven't thought 
through all of the consequences of that. It's all wrapped up in the very 
reason why Rebindable is in the library rather than being built into the 
language.

For starters, if you have

class C {}
C c;

the type of c is C. C is a reference to the class named C. It does _not_ mean 
the same thing as the C in the class declaration at all. If it were a struct 
rather than a class, it would be equivalent to C* (save for the fact that a 
reference and a pointer aren't quite the same). So, _everywhere_ in the type 
system that you see C, it's really C*, _not_ C as in the class C. There is _no 
way_ in the type system to refer to the class itself. So, making C* be a 
pointer to the object rather than the reference isn't even representable in 
the type system. It's exactly the same in Java and C# with their classes. 
Walter tried to get around this to implement Rebindable in the language and 
gave up on it. It may be possible, but it's _ugly_ if it is.

There are all kinds of practical side effects for this. For instance, if you 
had

class D : C {}

C c;
C* cPtr = &c;

and C* was a pointer to the class itself, then it would almost be like doing

C* c;
C* cPtr = &c;

which makes no sense at all. It also introduces object slicing - as in the 
type of slicing that C++ has when you assign a derived object to a base class 
object which is on the stack. In C++,

D d;
C c = d;

results in the D portion of d being chopped off, potentially putting it in an 
invalid state, and almost certainly isn't what you wanted to do. D avoids this 
by requiring that polymorphic objects (classes) be on the heap. But if C* 
referred to the object itself, it becomes a probem again.

C c = new C;
C* cPtr = &c;
*cPtr = new D;

Since cPtr was a pointer to the object, *cPtr would be the object itself, and 
so the D object would be assigned to the C object and get sliced, just like in 
the C++ example.

In additon, making Object* be a pointer to the object itself would make 
dealing with pointers to local objects _very_ inconsistent. Take this for 
example

void func(T)(T* t, T value)
{
*t = value;
}

int i;
func(&i, 7);

C c = getC();
func(&c, new C);

When func is called with &i, it sets i to 7. But when it's called with &c, it 
doesn't set c. It sets what c refers to. This means that instead of changing 
the local variable c, you've changed an object on the heap which other 
references could refer to, and now instead of just affecting the local 
reference, _every_ reference is affected. This is _completely_ different from 
how it works with &i. It's more in like with how it would work if you had

int* iPtr = getIntPtr();
func(&iPtr, new int);

and since C is essentially equivalent to C*, it would then be impossible to 
pass the reference itself to func to be set.

It would also make it so that taking a pointer for a parameter was very 
different from taking ref or out for classes, when it's nearly identical for 
everything else.

void refFunc(T)(ref T t, T value)
{
t = value;
}

int i;
refFunc(i, 7);

C c = getC();
refFunc(c, new C);

With your suggestion, this code operates identically for i but does something 
completely different for c. Now, instead of setting the object, it's setting 
the reference.

AAs would also be very broken if Object* pointed to the object rather than the 
reference. Take this code:

int[string] aa;
int* intPtr = "hello" in aa;

C[string] bb;
C* cPtr = "hello" in bb;

With aa, you get a pointer to the value which is at the key "hello". With bb, 
you get a pointer to the object which the value at "hello" refers to. So, once 
again, setting *cPtr sets the object rather than the reference, and slicing 
becomes a problem. On top of that, what happens with null? Right now, you can 
do

bb["hello"] = null;
C* v1 = "hello" in bb;
C* v2 = "world" in bb;

The fact that v1 is non-null tells you that "hello" is in bb, and the fact 
that v2 is null tells you that "world" isn't in bb. You can then dereference 
v1 and get at the value which is at "hello", which is null. But what happens 
when C* nows points to the object rather than the reference? It becomes 
impossible to distinguish between the case when the key isn't in the AA and 
when the value at that key is null. You could fix that by making it so that

C[string] bb;

was implicitly

C*[string] bb;

but then the type that in returns would have to be C**, making it so that 
Objects behaved differently with AAs than every other type, since in all other 
cases with T[U], in returns T*, not T**.

I could go on, but this post is already ridiculously long. The point is that 
the type system is built around the fact that class variables are _always_ 
references and that there is no way to refer to such an object directly. If 
you start referring to the object directly, things break. The type system just 
does not support that, and making &c give you pointer to the object rather 
than the reference would make it _completely_ inconsistent with the rest of 
the language, much as it might seem otherwise at first. You just have to 
realize that whenever you see Object (or any other class type) used, it's a 
reference, _not_ the type of the object itself, and that it's impossible 
(within the type system) to refer to the object itself, so the behavior of &c 
is _completely_ consistent with the rest of the language.

- Jonathan M Davis
July 12, 2012
Re: readonly?
On 07/12/12 01:09, Jonathan M Davis wrote:
> On Wednesday, July 11, 2012 10:56:23 Artur Skawina wrote:
>> Can anybody think of a reason to keep the current (broken) behavior?
> 
> Easily.

You misunderstand the "current (broken) behavior" part - it is about
what 'C*' is, it is not at all about class references.

> Making Object* point to the object itself rather than the reference would be 
> so broken that it's not even funny. I can understand why you would think that 
> such a change should be made, but that's only because you haven't thought 
> through all of the consequences of that. It's all wrapped up in the very 
> reason why Rebindable is in the library rather than being built into the 
> language.
> 
> For starters, if you have
> 
> class C {}
> C c;
> 
> the type of c is C. C is a reference to the class named C. It does _not_ mean 
> the same thing as the C in the class declaration at all. If it were a struct 
> rather than a class, it would be equivalent to C* (save for the fact that a 
> reference and a pointer aren't quite the same). So, _everywhere_ in the type 
> system that you see C, it's really C*, _not_ C as in the class C. There is _no 
> way_ in the type system to refer to the class itself. So, making C* be a 
> pointer to the object rather than the reference isn't even representable in 
> the type system.

Of course it is - you even say it above: "_everywhere_ in the type system that
you see C, it's really C*".
But instead of considering the implications and required semantics, you are 
making several assumptions, which are wrong.
For example - let 'C' be a class, then: 'new C' must return a reference to C,
not a pointer; dereferencing a 'C*' pointer should of course not be valid.


> There are all kinds of practical side effects for this. For instance, if you 
> had
> 
> class D : C {}
> 
> C c;
> C* cPtr = &c;
> 
> and C* was a pointer to the class itself, then it would almost be like doing
> 
> C* c;
> C* cPtr = &c;
> 
> which makes no sense at all.

This is actually what effectively happens with the *current* model, it in deed
makes little sense, and is the reason for my "why?" questions...


> It also introduces object slicing - as in the 
> type of slicing that C++ has when you assign a derived object to a base class 
> object which is on the stack. In C++,
> 
> D d;
> C c = d;
> 
> results in the D portion of d being chopped off, potentially putting it in an 

D and C (in D) are references - there's no problem. I have no idea why you think
classes would be value types.

> invalid state, and almost certainly isn't what you wanted to do. D avoids this 
> by requiring that polymorphic objects (classes) be on the heap. But if C* 
> referred to the object itself, it becomes a probem again.
> 
> C c = new C;
> C* cPtr = &c;
> *cPtr = new D;
> 
> Since cPtr was a pointer to the object, *cPtr would be the object itself, and 
> so the D object would be assigned to the C object and get sliced, just like in 
> the C++ example.

Dereferencing a class pointer has to be illegal, i thought that was obvious and
not needed to be spelled out. It's a simple and easily understandable rule.
So '*cPtr' wouldn't be valid - there is no problem.

> In additon, making Object* be a pointer to the object itself would make 
> dealing with pointers to local objects _very_ inconsistent. Take this for 
> example
> 
> void func(T)(T* t, T value)
> {
>  *t = value;
> }
> 
> int i;
> func(&i, 7);
> 
> C c = getC();
> func(&c, new C);
> 
> When func is called with &i, it sets i to 7. But when it's called with &c, it 
> doesn't set c. It sets what c refers to. This means that instead of changing 

'*t' would not compile for a T*==C* (when the latter means a direct
pointer to the instance) ; see above.

Whether that should compile, in the model i gave as an example, is a
different question - there the call is 'func(C**, C)' which results in
'C* = implicit_cast(C*)C' -- this modifies the reference. I can see
arguments for disallowing this, but then the 'rebindable' problem would
be back, so I'd lean toward keeping it legal.

> the local variable c, you've changed an object on the heap which other 
> references could refer to, and now instead of just affecting the local 
> reference, _every_ reference is affected. This is _completely_ different from 
> how it works with &i. It's more in like with how it would work if you had
> 
> int* iPtr = getIntPtr();
> func(&iPtr, new int);
> 
> and since C is essentially equivalent to C*, it would then be impossible to 
> pass the reference itself to func to be set.

That's why I have '&C_instance' result in 'C**'. not 'C*'. Yes, it means
classes are treated differently -- but they *already* are. Yes, it's *only*
about the type, most (ie all desirable) semantics of references are kept. 

> It would also make it so that taking a pointer for a parameter was very 
> different from taking ref or out for classes, when it's nearly identical for 
> everything else.
> 
> void refFunc(T)(ref T t, T value)
> {
>  t = value;
> }
> 
> int i;
> refFunc(i, 7);
> 
> C c = getC();
> refFunc(c, new C);
> 
> With your suggestion, this code operates identically for i but does something 
> completely different for c. Now, instead of setting the object, it's setting 
> the reference.

Huh? There are no (explicit) pointers involved here, there's no difference.
I think you didn't actually read my example carefully enough, as you keep
bringing up things that nobody suggested, and which wouldn't make any sense.

> AAs would also be very broken if Object* pointed to the object rather than the 
> reference. Take this code:
> 
> int[string] aa;
> int* intPtr = "hello" in aa;
> 
> C[string] bb;
> C* cPtr = "hello" in bb;
> 
> With aa, you get a pointer to the value which is at the key "hello". With bb, 
> you get a pointer to the object which the value at "hello" refers to. So, once 
> again, setting *cPtr sets the object rather than the reference, and slicing 
> becomes a problem. On top of that, what happens with null? Right now, you can 
> do
> 
> bb["hello"] = null;
> C* v1 = "hello" in bb;
> C* v2 = "world" in bb;
> 
> The fact that v1 is non-null tells you that "hello" is in bb, and the fact 
> that v2 is null tells you that "world" isn't in bb. You can then dereference 
> v1 and get at the value which is at "hello", which is null. But what happens 
> when C* nows points to the object rather than the reference? It becomes 
> impossible to distinguish between the case when the key isn't in the AA and 
> when the value at that key is null. You could fix that by making it so that
> 
> C[string] bb;
> 
> was implicitly
> 
> C*[string] bb;
> 
> but then the type that in returns would have to be C**, making it so that 
> Objects behaved differently with AAs than every other type, since in all other 
> cases with T[U], in returns T*, not T**.

The AA 'in' operator is one of the odder ones, but there's no problem.
Remember the '&C => C**' rule? 'typeof("abc" in bb)==C**'. Maybe it's
easier to understand when you realize that what happens underneath is
equivalent to declaring a 'C*[string] bb'.

> (within the type system) to refer to the object itself, so the behavior of &c 
> is _completely_ consistent with the rest of the language.

Nope.

But the question was if there is any advantage to the current model? And so
far I've seen none mentioned.

Note that one alternative is to basically treat 'C*' similarly to a reference
to C, which means there are no slicing or polymorphism issues, as the obvious
extra restrictions on such a type take care of things. This new model would be
much saner, while allowing things that are currently not (cleanly) possible.

I'm really just wondering if there was some real reason behind the design, or
if it was just an accident, because nobody considered the consequences - like
in the case of so many other D features.
This isn't about changing things now (at least not short term), it's about
future directions. 

artur
Next ›   Last »
1 2 3
Top | Discussion index | About this forum | D home