www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - const ref in opAssign

reply "monarch_dodra" <monarch_dodra gmail.com> writes:
I just finished reading the chapters on user defined types and, 
(as a C++ dev), this one line struc out to me as very odd:

"ref Widget opAssign(ref Widget rhs) {..}"

This means that during an assignment, I could potentially change 
"Other" (!). This seems like a blatant violation of the expected 
behavior of =. What's more, it prevents the assignment from a 
const object...

I am very tempted to change the call to "const ref". Is this 
un-advised?

Mr. Alexandrescu goes on to mention a "pass by value" in case you 
wanted to assign from a temporary, mentioning:
w = Widget(50); // Error!
// Cannot bind an rvalue of type Widget to ref Widget!

The only problem is that if I do this, all of my calls are then 
re-routed to pass by value, and none to the pass-by-const-ref.

----

Is there any way to enforce const correctness, while still 
keeping advantage of both calls?

PS: using Mr. Alexandrescu's design, it is not possible to assign 
from a const Widget, I get:
hello.d(50): Error: function hello.Widget.opAssign (Widget rhs) 
is not callable using argument types (const(Widget))
hello.d(50): Error: cannot implicitly convert expression (w2) of 
type const(Widget) to Widget

const can't be passed by value...?
Jun 26 2012
next sibling parent reply Jonathan M Davis <jmdavisProg gmx.com> writes:
On Tuesday, June 26, 2012 14:44:45 monarch_dodra wrote:
 I just finished reading the chapters on user defined types and,
 (as a C++ dev), this one line struc out to me as very odd:
 
 "ref Widget opAssign(ref Widget rhs) {..}"
 
 This means that during an assignment, I could potentially change
 "Other" (!). This seems like a blatant violation of the expected
 behavior of =. What's more, it prevents the assignment from a
 const object...
 
 I am very tempted to change the call to "const ref". Is this
 un-advised?
 
 Mr. Alexandrescu goes on to mention a "pass by value" in case you
 wanted to assign from a temporary, mentioning:
 w = Widget(50); // Error!
 // Cannot bind an rvalue of type Widget to ref Widget!
 
 The only problem is that if I do this, all of my calls are then
 re-routed to pass by value, and none to the pass-by-const-ref.
 
 ----
 
 Is there any way to enforce const correctness, while still
 keeping advantage of both calls?
 
 PS: using Mr. Alexandrescu's design, it is not possible to assign
 from a const Widget, I get:
 hello.d(50): Error: function hello.Widget.opAssign (Widget rhs)
 is not callable using argument types (const(Widget))
 hello.d(50): Error: cannot implicitly convert expression (w2) of
 type const(Widget) to Widget
 
 const can't be passed by value...?
Depending on your type, taking const ref Widget would be a big problem, because you wouldn't be able to assign a const member variable's value of rhs to a non-const member variable of this (e.g. if it's a class) without making a deep copy. Or it could be just fine. It all depends on your type and what you're trying to do. Regardless, the solution is almost certainly to have multiple overloads. Each of the overloads does this // Works with any Widget if Widget is a value type and any non-const Widget // if it's a reference type. ref Widget opAssign(Widget rhs) {} // Works with any Widget as long as you can copy all of the member variables // when they're const. ref Widget opAssign(const Widget rhs) {} // Works with non-const Widgets which are lvalues only. ref Widget opAssign(ref Widget rhs) {} // Works with any Widgets which are lvalues as long as you can copy all of the // member variables when they're const. ref Widget opAssign(ref Widget rhs) {} In most cases, I would expect you to have these two overloads ref Widget opAssign(const Widget rhs) {} ref Widget opAssign(const ref Widget rhs) {} It then works with const (as long as you're not dealing with a type which can't really be copied when it's const) and both rvalues and lvalues. The rvalue version must be const to ensure that constness does not affect which overload gets called (just l/rvalue-ness). This is particularly critical if the rvalue version simply calls the lvalue version, because if it's not const, you'd get infinite recursion. - Jonathan M Davis
Jun 26 2012
parent reply "monarch_dodra" <monarch_dodra gmail.com> writes:
On Tuesday, 26 June 2012 at 15:40:48 UTC, Jonathan M Davis wrote:
 On Tuesday, June 26, 2012 14:44:45 monarch_dodra wrote:

 Depending on your type, taking const ref Widget would be a big 
 problem,
 because you wouldn't be able to assign a const member 
 variable's value of rhs
 to a non-const member variable of this (e.g. if it's a class) 
 without making a
 deep copy. Or it could be just fine. It all depends on your 
 type and what
 you're trying to do. Regardless, the solution is almost 
 certainly to have
 multiple overloads. Each of the overloads does this

 // Works with any Widget if Widget is a value type and any 
 non-const Widget
 // if it's a reference type.
 ref Widget opAssign(Widget rhs) {}

 // Works with any Widget as long as you can copy all of the 
 member variables
 // when they're const.
 ref Widget opAssign(const Widget rhs) {}

 // Works with non-const Widgets which are lvalues only.
 ref Widget opAssign(ref Widget rhs) {}

 // Works with any Widgets which are lvalues as long as you can 
 copy all of the
 // member variables when they're const.
 ref Widget opAssign(ref Widget rhs) {}

 In most cases, I would expect you to have these two overloads

 ref Widget opAssign(const Widget rhs) {}
 ref Widget opAssign(const ref Widget rhs) {}

 It then works with const (as long as you're not dealing with a 
 type which
 can't really be copied when it's const) and both rvalues and 
 lvalues. The
 rvalue version must be const to ensure that constness does not 
 affect which
 overload gets called (just l/rvalue-ness). This is particularly 
 critical if
 the rvalue version simply calls the lvalue version, because if 
 it's not const,
 you'd get infinite recursion.

 - Jonathan M Davis
Thanks for the in-depth explanation! It is still not very clear to me, mostly because I don't understand "you wouldn't be able to assign a const member variable's value of rhs to a non-const member variable of this". This works fine in C++. Must be because I've never used a language with reference semantics before. Either that, or I'm miss-understanding the "ref" keyword. I thought "const ref" meant pass by const reference, but it would appear it means a reference to a const object... or something like that. I'll need to read the chapter on const too. Either way, I guess I'll have to become more familiar with the language to fully appreciate this.
Jun 26 2012
next sibling parent travert phare.normalesup.org (Christophe Travert) writes:
"monarch_dodra" , dans le message (digitalmars.D:170728), a écrit :
 Thanks for the in-depth explanation! It is still not very clear 
 to me, mostly because I don't understand "you wouldn't be able to 
 assign a const member variable's value of rhs to a non-const 
 member variable of this". This works fine in C++. Must be because 
 I've never used a language with reference semantics before.
In D, const is transitive. It mean that if an instance is const, everything that is refered by this instance is const. Thus, I can't copy a reference of a const instance to make a non-const reference. Example: struct A { int* x; ref A opAssign(const ref other) { this.x = other.x; // error: other.x, which is a const(int*) because // of transitivity, can't be assign to this.x // (which is a non-const int*). } } You need to write either: ref A opAssign(ref other) { this.x = other.x; // no problem, other.x is not const. // Note: the value this.x is now shared between this and other } or: ref A opAssign(const ref other) // deep copy { this.x = new int(*other.x); // this is a deep copy: a new int is created to take other.x's value } If the structure contain no references at all, there is no problem, and opAssign should be const ref, with a const overload for l-values, as previously said. -- Christophe
Jun 26 2012
prev sibling parent "Jonathan M Davis" <jmdavisProg gmx.com> writes:
On Tuesday, June 26, 2012 18:32:37 monarch_dodra wrote:
 Thanks for the in-depth explanation! It is still not very clear
 to me, mostly because I don't understand "you wouldn't be able to
 assign a const member variable's value of rhs to a non-const
 member variable of this". This works fine in C++. Must be because
 I've never used a language with reference semantics before.
 
 Either that, or I'm miss-understanding the "ref" keyword. I
 thought "const ref" meant pass by const reference, but it would
 appear it means a reference to a const object... or something
 like that. I'll need to read the chapter on const too.
 
 Either way, I guess I'll have to become more familiar with the
 language to fully appreciate this.
Unlike in C++, a const ref (or const T& in C++) will not accept rvalues. The only difference between ref and const ref is that const ref takes const lvalues. There has been some discussion on changing this, but apparently there are some issues that arise from accepting rvalues for const& the way that C++ does which we were trying to avoid, so const ref does not accept rvalues. As for "you wouldn't be able to assign a const member variable's value of rhs to a non-const member of this," the problem is that D's const is transitive. Once part of an object is const, _everything_ that it refers to is const, and casting away const and mutating the object is undefined behavior (unlike C/C++). This provides much stronger guarantees for const and is required to properly support immutable, but it _is_ more restrictive. This means that if you have a const object and you want to assign it to a non-const object, if it's not a value type, the _only_ way to do it without subverting the type system is to create a deep copy. So, if you have struct S { T* value; } the opAssign for S would have to make a deep copy of value rather than simply assigning rhs' value to S' value. That's not entirely unlike C++ (especially if you're not casting away const in C++) - and you often want to make a deep copy anyway - but the additional strictness of D's const makes it so that there are more cases where you have to worry about and work around constness than you would in C++. - Jonathan M Davis
Jun 26 2012
prev sibling next sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 06/26/2012 02:44 PM, monarch_dodra wrote:
 Is there any way to enforce const correctness, ...
Note that 'const correctness' is a C++ term that does not really have an obvious counterpart in D.
Jun 26 2012
parent reply "Jonathan M Davis" <jmdavisProg gmx.com> writes:
On Tuesday, June 26, 2012 20:10:59 Timon Gehr wrote:
 On 06/26/2012 02:44 PM, monarch_dodra wrote:
 Is there any way to enforce const correctness, ...
Note that 'const correctness' is a C++ term that does not really have an obvious counterpart in D.
Really? I'd have said that a type was const correct if every one of its functions that could be const was const, and that applies to D as much as C++. It's just that you don't have the same issues with types subverting const in D that you have in C++, since there is no mutable keyword and casting away const and mutating the object is undefined. - Jonathan M Davis
Jun 26 2012
parent Timon Gehr <timon.gehr gmx.ch> writes:
On 06/26/2012 08:40 PM, Jonathan M Davis wrote:
 On Tuesday, June 26, 2012 20:10:59 Timon Gehr wrote:
 On 06/26/2012 02:44 PM, monarch_dodra wrote:
 Is there any way to enforce const correctness, ...
Note that 'const correctness' is a C++ term that does not really have an obvious counterpart in D.
Really? I'd have said that a type was const correct if every one of its functions that could be const was const, and that applies to D as much as C++.
In C++ _every_ method can be const -- it is not more than conventionalised interface documentation. In D, there is no way to tell whether some virtual method can be const without taking into account the entire code base. This makes this definition of const correctness moot immediately for both C++ and D.
 It's just that you don't have the same issues with types subverting const in D
 that you have in C++, since there is no mutable keyword and casting away const
 and mutating the object is undefined.

 - Jonathan M Davis
Exactly, there is no obvious counterpart.
Jun 26 2012
prev sibling parent "Era Scarecrow" <rtcvb32 yahoo.com> writes:
On Tuesday, 26 June 2012 at 12:44:46 UTC, monarch_dodra wrote:
 I just finished reading the chapters on user defined types and, 
 (as a C++ dev), this one line struc out to me as very odd:

 "ref Widget opAssign(ref Widget rhs) {..}"

 This means that during an assignment, I could potentially 
 change "Other" (!). This seems like a blatant violation of the 
 expected behavior of =. What's more, it prevents the assignment 
 from a const object...

 I am very tempted to change the call to "const ref". Is this 
 un-advised?

 Mr. Alexandrescu goes on to mention a "pass by value" in case 
 you wanted to assign from a temporary, mentioning:
 w = Widget(50); // Error!
 // Cannot bind an rvalue of type Widget to ref Widget!

 The only problem is that if I do this, all of my calls are then 
 re-routed to pass by value, and none to the pass-by-const-ref.

 ----

 Is there any way to enforce const correctness, while still 
 keeping advantage of both calls?

 PS: using Mr. Alexandrescu's design, it is not possible to 
 assign from a const Widget, I get:
 hello.d(50): Error: function hello.Widget.opAssign (Widget rhs) 
 is not callable using argument types (const(Widget))
 hello.d(50): Error: cannot implicitly convert expression (w2) 
 of type const(Widget) to Widget

 const can't be passed by value...?
I actually remember having this issue (I'll look up the reference to it later). The issue was the non-const non-ref version was a better fit; Now obviously it's wrong, but you can't convince the compiler of that. If the assign never modifies the incoming object, then why should you make two versions? Anyways. Try making 2 versions of opAssign with ref and try it again: ie a const-ref & non const-ref, and finally your pass by value.
Jun 26 2012