www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - disable all member function calls for rvalues?

reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
There is a wart in D I'd like to eliminate. Unfortunately, that would 
also eliminate some valid uses.

The problem is as follows. "ref" values in function parameter lists are 
always bound to lvalues - never rvalues. This is, except for _one_ case:
"this" in member functions.

struct A {
    void fun() {
       ...
    }
}

A gun() { return A(); }

unittest {
    gun().fun(); // works
}

This doesn't sound so bad until the following happens:

struct A {
    ref A opAssign(ref A rhs) { ... }
}

struct B {
     property A a() { ... }
}

unittest {
    B b;
    b.a = A.init;
}

Everything looks kosher, but the problem is b.a returns a temporary, and 
then that temporary is assigned to. The assignment is inoperant.

For this and other related reasons I'd like to disallow binding 
temporaries to "this". There are cases in which you'd want to, and you'd 
have to insert explicit variables. But I think eliminating the binding 
is the right thing to do.

Thoughts?


Andrei
Dec 17 2009
next sibling parent reply dsimcha <dsimcha yahoo.com> writes:
== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s article
 There is a wart in D I'd like to eliminate. Unfortunately, that would
 also eliminate some valid uses.

This is the problem, with this feature and with a lot of other "safety" features that have been considered. Having lots of static checking, etc. is good if and only if it doesn't get in the way too much when the programmer **does** know what he/she is doing. Yes, this limitation would prevent some bugs, but it would get in the way very frequently as well and be an extremely annoying piece of syntactic salt. Given the choice I'd rather have to debug a few more bugs in corner cases than have the language reject lots of valid cases. Of course, there's room for intermediate solutions: 1. Allow member function calls iff the function is const. For example, the following would still work: struct A { string toString() const { return "A"; } } struct B { A getA() property { return A.init; } } B b; string s = b.getA().toString(); 2. Special case opAssign, since this clearly makes no sense for rvalues. 3. If consistency is the main concern, make other ref rvalue binding more permissive instead.
Dec 17 2009
parent Jason House <jason.james.house gmail.com> writes:
dsimcha Wrote:

 == Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s article
 There is a wart in D I'd like to eliminate. Unfortunately, that would
 also eliminate some valid uses.

This is the problem, with this feature and with a lot of other "safety" features that have been considered. Having lots of static checking, etc. is good if and only if it doesn't get in the way too much when the programmer **does** know what he/she is doing. Yes, this limitation would prevent some bugs, but it would get in the way very frequently as well and be an extremely annoying piece of syntactic salt. Given the choice I'd rather have to debug a few more bugs in corner cases than have the language reject lots of valid cases. Of course, there's room for intermediate solutions: 1. Allow member function calls iff the function is const. For example, the following would still work: struct A { string toString() const { return "A"; } }

immutable functions too.
 struct B {
     A getA()  property {
         return A.init;
     }
 }
 
 B b;
 string s = b.getA().toString();
 
 2.  Special case opAssign, since this clearly makes no sense for rvalues.
 
 3.  If consistency is the main concern, make other ref rvalue binding more
 permissive instead.

An alternate I was thinking about was disallowing non-const/immutable functions with void return type. They seem the most probable to be doing bad things. Some may want the read portion of a property to be callable on an rvalue. That could allow use of "logical const" by convention...
Dec 17 2009
prev sibling next sibling parent dsimcha <dsimcha yahoo.com> writes:
== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s article
 There is a wart in D I'd like to eliminate. Unfortunately, that would
 also eliminate some valid uses.

I'll say. One more example that I forgot to include, but I think is pretty important: struct CheckedInt { int num; /// Rest of implementation. bool opEquals(CheckedInt rhs) const { return num == rhs.num; } } CheckedInt fun() { return CheckedInt(5); } assert(fun() == CheckedInt(5)); // Wouldn't compile. Requiring an explicit temp variable here is so utterly ridiculous that, if D required one, it would rightfully be made the laughing stock among programming languages.
Dec 17 2009
prev sibling next sibling parent Michel Fortin <michel.fortin michelf.com> writes:
On 2009-12-17 23:57:09 -0500, Andrei Alexandrescu 
<SeeWebsiteForEmail erdani.org> said:

 There is a wart in D I'd like to eliminate. Unfortunately, that would 
 also eliminate some valid uses.
 
 The problem is as follows. "ref" values in function parameter lists are 
 always bound to lvalues - never rvalues. This is, except for _one_ case:
 "this" in member functions.
 
 struct A {
     void fun() {
        ...
     }
 }
 
 A gun() { return A(); }
 
 unittest {
     gun().fun(); // works
 }
 
 This doesn't sound so bad until the following happens:
 
 struct A {
     ref A opAssign(ref A rhs) { ... }
 }
 
 struct B {
      property A a() { ... }
 }
 
 unittest {
     B b;
     b.a = A.init;
 }
 
 Everything looks kosher, but the problem is b.a returns a temporary, 
 and then that temporary is assigned to. The assignment is inoperant.

Inoperant? Not necessarily. Assigning to temporaries like that is used sometime in C++ when a temporary "proxy" object is returned, the temporary object just forwarding the assignment to the right place. Calling any function on a temporary could have "side effects", and those "side effects" might effectively be the main desired effect. Which makes me think that the same applies to rvalues being bound to ref: they could be temporary proxies too, or other types with side effects.
 For this and other related reasons I'd like to disallow binding 
 temporaries to "this". There are cases in which you'd want to, and 
 you'd have to insert explicit variables. But I think eliminating the 
 binding is the right thing to do.

I agree with you that the dissimilarity for this and other arguments is not desirable. Personally, I would allow rvalues to convert to non-const ref. There are as many valid uses for temporaries passed as arguments (like the proxy object) than there are for temporaries passed as "this". If you really want to disallow ref temporaries, then I'd suggest having some kind of flag allowing us to opt-in specific types. Only those types would be allowed as non-const ref temporaries. But it seem to me that we already have enough flags to deal with in D. Which makes me think of another problem with forcing const: unlike C++, our const is transitive. This means that if your temporary is just some kind of smart pointer to somewhere, forcing it to be const would prevent any modification of the data it points to. Simple synthetic example: RefCountedPtr!MyObject getAnObject(); void test(ref RefCountedPtr!MyObject object) { object.mutate(); // method that requires a mutable object } test(getAnObject); Anything of the sort can't work if you force the argument to be const. In C++ you can make ref const with not much consequence because const is not transitive. But not in D. I'm all for detecting unintended uses of rvalues, but I think forcing ref to be const is a mistake, both for "this" and other arguments. We need another solution. -- Michel Fortin michel.fortin michelf.com http://michelf.com/
Dec 18 2009
prev sibling next sibling parent reply Kagamin <spam here.lot> writes:
Andrei Alexandrescu Wrote:

     b.a = A.init;

The problem here is not in passing temporaries as this, the problem is that property assignment is ambiguous with opAssign called on the property's return value. Your proposed solution seems unrelated to the problem.
Dec 18 2009
next sibling parent reply Kagamin <spam here.lot> writes:
Kagamin Wrote:

 Andrei Alexandrescu Wrote:
 
     b.a = A.init;

The problem here is not in passing temporaries as this, the problem is that property assignment is ambiguous with opAssign called on the property's return value. Your proposed solution seems unrelated to the problem.

I think, possible solution is to disallow assign operations on the return value: =, +=, *=, ~=, ++ etc, but read operations should be allowed and functions should be callable by their symbolical names.
Dec 18 2009
parent reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
Kagamin wrote:
 Kagamin Wrote:
 
 Andrei Alexandrescu Wrote:

     b.a = A.init;


I think, possible solution is to disallow assign operations on the return value: =, +=, *=, ~=, ++ etc, but read operations should be allowed and functions should be callable by their symbolical names.

Won't help. Consider: a.b.c.d.e.f = g; If anywhere on the path an rvalue is created, the code is useless busywork. Andrei
Dec 18 2009
parent reply Kagamin <spam here.lot> writes:
Andrei Alexandrescu Wrote:

 I think, possible solution is to disallow assign operations on the return
value: =, +=, *=, ~=, ++ etc, but read operations should be allowed and
functions should be callable by their symbolical names.

Won't help. Consider: a.b.c.d.e.f = g; If anywhere on the path an rvalue is created, the code is useless busywork.

In contrast with the original example this is not a bug.
Dec 22 2009
parent reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
Kagamin wrote:
 Andrei Alexandrescu Wrote:
 
 I think, possible solution is to disallow assign operations on the return
value: =, +=, *=, ~=, ++ etc, but read operations should be allowed and
functions should be callable by their symbolical names.

a.b.c.d.e.f = g; If anywhere on the path an rvalue is created, the code is useless busywork.

In contrast with the original example this is not a bug.

I think it is to the extent (a) it does nothing (b) the syntactic equivalent code involving fields does something. It's an egregious breakage of consistency because properties were meant to be generalizations of fields in the first place. Andrei
Dec 22 2009
next sibling parent Michel Fortin <michel.fortin michelf.com> writes:
On 2009-12-22 08:45:00 -0500, "Steven Schveighoffer" 
<schveiguy yahoo.com> said:

 The point is, wrapper types shouldn't have second-class citizenship as  
 rvalues to pointers and references.  There's got to be a way to 
 identify  types as being lvalues even though the compiler doesn't see 
 it that way.   If you can accomplish that, then I have no problem 
 disallowing setting  members of true rvalues.

What you need is tail const. Lvalues are similar to tail-const values: can't change them, but can change what they point to. The lvalueness is not transitive, so a transitive const isn't appropriate for representing lvalues. Then of course you need to annotate member functions as being tail-const, and there the const system becomes more complicated. -- Michel Fortin michel.fortin michelf.com http://michelf.com/
Dec 22 2009
prev sibling parent reply Kagamin <spam here.lot> writes:
Andrei Alexandrescu Wrote:

 In contrast with the original example this is not a bug.

I think it is to the extent (a) it does nothing (b) the syntactic equivalent code involving fields does something. It's an egregious breakage of consistency because properties were meant to be generalizations of fields in the first place.

class File { ... } struct MyStruct { File obj; File obj2(){ return obj; } } MyStruct foo(); foo().obj.unlink(); foo().obj2().unlink(); Why do you think this is invalid?
Dec 23 2009
parent reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
Kagamin wrote:
 Andrei Alexandrescu Wrote:
 
 In contrast with the original example this is not a bug.

equivalent code involving fields does something. It's an egregious breakage of consistency because properties were meant to be generalizations of fields in the first place.

class File { ... } struct MyStruct { File obj; File obj2(){ return obj; } } MyStruct foo(); foo().obj.unlink(); foo().obj2().unlink(); Why do you think this is invalid?

I didn't think that's invalid. I was talking about the other example that looked like a field assignment but did not assign to a field. File does not have an unlink member but I get your point. As I said, refusing to bind ref to rvalues disables a few valid uses. I am willing to renounce those few uses. Andrei
Dec 23 2009
parent reply Michel Fortin <michel.fortin michelf.com> writes:
On 2009-12-23 09:15:18 -0500, Andrei Alexandrescu 
<SeeWebsiteForEmail erdani.org> said:

 I didn't think that's invalid. I was talking about the other example 
 that looked like a field assignment but did not assign to a field.
 
 File does not have an unlink member but I get your point. As I said, 
 refusing to bind ref to rvalues disables a few valid uses. I am willing 
 to renounce those few uses.

Is this one of them? int[5] array = [1, 2, 3, 4, 5]; array[1..3] = [0, 0]; Or, more likely returning a non-built-in range type is one of them: MyContainer container = [1, 2, 3, 4, 5]; container[1..3] = [0, 0]; and you'll have to write it like this: MyContainer container = [1, 2, 3, 4, 5]; MyRange range = container[1..3]; range = [0, 0]; -- Michel Fortin michel.fortin michelf.com http://michelf.com/
Dec 23 2009
parent reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
Michel Fortin wrote:
 On 2009-12-23 09:15:18 -0500, Andrei Alexandrescu 
 <SeeWebsiteForEmail erdani.org> said:
 
 I didn't think that's invalid. I was talking about the other example 
 that looked like a field assignment but did not assign to a field.

 File does not have an unlink member but I get your point. As I said, 
 refusing to bind ref to rvalues disables a few valid uses. I am 
 willing to renounce those few uses.

Is this one of them? int[5] array = [1, 2, 3, 4, 5]; array[1..3] = [0, 0]; Or, more likely returning a non-built-in range type is one of them: MyContainer container = [1, 2, 3, 4, 5]; container[1..3] = [0, 0]; and you'll have to write it like this: MyContainer container = [1, 2, 3, 4, 5]; MyRange range = container[1..3]; range = [0, 0];

That's just mean! :o) Guess I need to acquiesce. Thanks Michel for the example. Andrei
Dec 23 2009
parent Michel Fortin <michel.fortin michelf.com> writes:
On 2009-12-23 12:53:54 -0500, Andrei Alexandrescu 
<SeeWebsiteForEmail erdani.org> said:

 Is this one of them?
 
     int[5] array = [1, 2, 3, 4, 5];
     array[1..3] = [0, 0];
 
 Or, more likely returning a non-built-in range type is one of them:
 
     MyContainer container = [1, 2, 3, 4, 5];
     container[1..3] = [0, 0];
 
 and you'll have to write it like this:
 
     MyContainer container = [1, 2, 3, 4, 5];
     MyRange range = container[1..3];
     range = [0, 0];

That's just mean! :o) Guess I need to acquiesce. Thanks Michel for the example.

Well, if we had a way for saying we want to pass the "this" argument by value, it'd solve the problem nicely. For the example above to work: struct MyRange { void opAssign(...) byvalue; } Only functions with the byvalue attribute (as well as const and immutable functions) could be called on an lvalue. So you could allow assignment and disallow changing the length of the range on an lvalue. -- Michel Fortin michel.fortin michelf.com http://michelf.com/
Dec 24 2009
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Tue, 22 Dec 2009 08:30:33 -0500, Andrei Alexandrescu  
<SeeWebsiteForEmail erdani.org> wrote:

 Kagamin wrote:
 Andrei Alexandrescu Wrote:

 I think, possible solution is to disallow assign operations on the  
 return value: =, +=, *=, ~=, ++ etc, but read operations should be  
 allowed and functions should be callable by their symbolical names.

a.b.c.d.e.f = g; If anywhere on the path an rvalue is created, the code is useless busywork.


I think it is to the extent (a) it does nothing (b) the syntactic equivalent code involving fields does something. It's an egregious breakage of consistency because properties were meant to be generalizations of fields in the first place.

What if a.b returns a Rebindable!(const(T)) rvalue, and the rest are lvalues? -Steve
Dec 22 2009
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Tue, 22 Dec 2009 08:41:11 -0500, Steven Schveighoffer  
<schveiguy yahoo.com> wrote:

 On Tue, 22 Dec 2009 08:30:33 -0500, Andrei Alexandrescu  
 <SeeWebsiteForEmail erdani.org> wrote:

 Kagamin wrote:
 Andrei Alexandrescu Wrote:

 I think, possible solution is to disallow assign operations on the  
 return value: =, +=, *=, ~=, ++ etc, but read operations should be  
 allowed and functions should be callable by their symbolical names.

a.b.c.d.e.f = g; If anywhere on the path an rvalue is created, the code is useless busywork.


I think it is to the extent (a) it does nothing (b) the syntactic equivalent code involving fields does something. It's an egregious breakage of consistency because properties were meant to be generalizations of fields in the first place.

What if a.b returns a Rebindable!(const(T)) rvalue, and the rest are lvalues?

I just realized in my efforts to use a real phobos-supported wrapper type, I doomed my own argument :) The point is, wrapper types shouldn't have second-class citizenship as rvalues to pointers and references. There's got to be a way to identify types as being lvalues even though the compiler doesn't see it that way. If you can accomplish that, then I have no problem disallowing setting members of true rvalues. -Steve
Dec 22 2009
prev sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Tue, 22 Dec 2009 11:05:52 -0500, Michel Fortin  
<michel.fortin michelf.com> wrote:

 On 2009-12-22 08:45:00 -0500, "Steven Schveighoffer"  
 <schveiguy yahoo.com> said:

 The point is, wrapper types shouldn't have second-class citizenship as   
 rvalues to pointers and references.  There's got to be a way to  
 identify  types as being lvalues even though the compiler doesn't see  
 it that way.   If you can accomplish that, then I have no problem  
 disallowing setting  members of true rvalues.

What you need is tail const. Lvalues are similar to tail-const values: can't change them, but can change what they point to. The lvalueness is not transitive, so a transitive const isn't appropriate for representing lvalues. Then of course you need to annotate member functions as being tail-const, and there the const system becomes more complicated.

You mean head-const. We have tail-const already (I often get them confused too). But that's not exactly what I mean. Constancy aside, if I have a wrapper struct like this: class C { void setX(int n){...} property S s() {return S(this);} } struct S { private C c; property void x(int n) { c.setX(n); } } I should be able to do this: auto c = new C; c.s.x = 5; Basically, s is giving a restrictive interface to a C, using the power of struct member functions, I can filter what functions are available or what they are named, or log them, or do whatever I want. It's a wrapper type. If s's functions are inlined, then S has almost no overhead, it just provides static interface transformation. Even if there was an annotation like: lvalue struct S { } which hints to the compiler that S can be used as an lvalue, I'd be more than happy to accept Andrei's proposed restriction. -Steve
Dec 22 2009
prev sibling parent Kagamin <spam here.lot> writes:
Andrei Alexandrescu Wrote:

 you'd have to insert explicit variables.

Yesterday I hit bug 1893 and I won't apreciate any extra requirement for explicit meaningless temporaries.
Dec 18 2009