Thread overview
const ref in opAssign
Jun 26, 2012
monarch_dodra
Jun 26, 2012
Jonathan M Davis
Jun 26, 2012
monarch_dodra
Jun 26, 2012
Christophe Travert
Jun 26, 2012
Jonathan M Davis
Jun 26, 2012
Timon Gehr
Jun 26, 2012
Jonathan M Davis
Jun 26, 2012
Timon Gehr
Jun 26, 2012
Era Scarecrow
June 26, 2012
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...?
June 26, 2012
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
June 26, 2012
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.
June 26, 2012
"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
June 26, 2012
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
June 26, 2012
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.
June 26, 2012
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
June 26, 2012
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.
June 26, 2012
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.