www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Generated opAssign in the presence of a copy constructor

reply RazvanN <razvan.nitu1305 gmail.com> writes:
Hello everyone!

As you probably know, I am working on the copy constructor DIP 
and implementation. So far, I managed to implement 95% of the 
copy constructor logic (as stated in the DIP). The point that is 
a bit of a headache is how/when should opAssign be generated when 
a copy constructor is defined. Now here is what I have (big 
thanks to Andrei for all the ideas, suggestions and brainstorms):

-> mutability of struct fields:

If the struct contains any const/immutable fields, it is 
impossible to use the copy constructor for opAssign, because the 
copy constructor might initialize them. Even if the copy 
constructor doesn't touch the const/immutable fields the compiler 
has to analyze the function body to know that, which is 
problematic in situations when the body is missing. => opAssign 
will be generated when the struct contains only assignable 
(mutable) fields.

-> qualifiers:

The copy constructor signature is : ` implicit this(ref $q1 S 
rhs) $q2`, where q1 and q2 represent the qualifiers that can be 
applied to the function and the parameter (const, immutable, 
shared, etc.). The problem that arises is: depending on the 
values of $q1 and $q2 what should the signature of opAssign be?

A solution might be to generate for every copy constructor 
present its counterpart opAssign: `void opAssign(ref $q1 S rhs) 
$q2`. However, when is a const/immutable opAssign needed? There 
might be obscure cases when that is useful, but those are niche 
situations where the user must step it and clarify what the 
desired outcome is and define its own opAssign. For the sake of 
simplicity, opAssign will be generated solely for copy 
constructors that have a missing $q2 = ``.

-> semantics in the presence of a destructor:

If the struct that has a copy constructor does not define a 
destructor, it is easy to create the body of the above-mentioned 
opAssign: the copy constructor is called and that's that:

void opAssign(ref $q1 S rhs)    // version 1
{
     S tmp = rhs;        // copy constructor is called
     memcpy(this, tmp);  // blit it into this
}

Things get interesting when a destructor is defined, because now 
we also have to call it on the destination:

void opAssign(ref $q1 S rhs)   // version 2
{
    this.__dtor;           // ensure the dtor is called
    memcpy(this, S.init)   // bring the object in the initial state
    this.copyCtor(rhs);    // call constructor on object in .init 
state
}

The problem with the above solution is that it does not take into 
account the fact
that the copyCtor may throw and if it does, then the object will 
be in a partially initialized state. In order to overcome this, 
two temporaries are used:

void opAssign(ref $q1 S rhs)    // version 3
{
     S tmp1 = rhs;                // call copy constructor
     void[S.sizeof] tmp2 = void;

     // swapbits(tmp1, this);
     memcpy(tmp2, this);
     memcpy(this, tmp1);
     memcpy(tmp1, tmp2);

     tmp1.__dtor();
}

In this version, if the copy constructor throws the object will 
still be in a valid state.

-> attribute inference for the generated opAssign:

For version 1: opAssign attributes are inferred based on the copy 
constructor attrbiutes.
For version 2: opAssign attributes are inferred based on copy 
constructor and destructor attributes
For version 3: the declaration of the void array can be put 
inside a trusted block and then attributes are inferred based on 
copy constructor and destructor attributes

If the copy constructor is marked `nothrow` and the struct 
defines a destructor, then version 2 is used, otherwise version 3.

What are your thoughts on this?

RazvanN
Jul 26 2018
next sibling parent reply Manu <turkeyman gmail.com> writes:
On Thu., 26 Jul. 2018, 2:45 am RazvanN via Digitalmars-d, <
digitalmars-d puremagic.com> wrote:

 Hello everyone!

 As you probably know, I am working on the copy constructor DIP
 and implementation. So far, I managed to implement 95% of the
 copy constructor logic (as stated in the DIP). The point that is
 a bit of a headache is how/when should opAssign be generated when
 a copy constructor is defined. Now here is what I have (big
 thanks to Andrei for all the ideas, suggestions and brainstorms):

 -> mutability of struct fields:

 If the struct contains any const/immutable fields, it is
 impossible to use the copy constructor for opAssign, because the
 copy constructor might initialize them. Even if the copy
 constructor doesn't touch the const/immutable fields the compiler
 has to analyze the function body to know that, which is
 problematic in situations when the body is missing. => opAssign
 will be generated when the struct contains only assignable
 (mutable) fields.

 -> qualifiers:

 The copy constructor signature is : ` implicit this(ref $q1 S
 rhs) $q2`, where q1 and q2 represent the qualifiers that can be
 applied to the function and the parameter (const, immutable,
 shared, etc.). The problem that arises is: depending on the
 values of $q1 and $q2 what should the signature of opAssign be?

 A solution might be to generate for every copy constructor
 present its counterpart opAssign: `void opAssign(ref $q1 S rhs)
 $q2`. However, when is a const/immutable opAssign needed? There
 might be obscure cases when that is useful, but those are niche
 situations where the user must step it and clarify what the
 desired outcome is and define its own opAssign. For the sake of
 simplicity, opAssign will be generated solely for copy
 constructors that have a missing $q2 = ``.

 -> semantics in the presence of a destructor:

 If the struct that has a copy constructor does not define a
 destructor, it is easy to create the body of the above-mentioned
 opAssign: the copy constructor is called and that's that:

 void opAssign(ref $q1 S rhs)    // version 1
 {
      S tmp = rhs;        // copy constructor is called
      memcpy(this, tmp);  // blit it into this
 }
Why the memcpy? This looks inefficient. Is it in case the constructor throws? Have a 'nothrow' case where it constructs directly to this? Things get interesting when a destructor is defined, because now
 we also have to call it on the destination:

 void opAssign(ref $q1 S rhs)   // version 2
 {
     this.__dtor;           // ensure the dtor is called
     memcpy(this, S.init)   // bring the object in the initial state
     this.copyCtor(rhs);    // call constructor on object in .init
 state
 }

 The problem with the above solution is that it does not take into
 account the fact
 that the copyCtor may throw and if it does, then the object will
 be in a partially initialized state. In order to overcome this,
 two temporaries are used:

 void opAssign(ref $q1 S rhs)    // version 3
 {
      S tmp1 = rhs;                // call copy constructor
      void[S.sizeof] tmp2 = void;

      // swapbits(tmp1, this);
      memcpy(tmp2, this);
      memcpy(this, tmp1);
      memcpy(tmp1, tmp2);

      tmp1.__dtor();
 }

 In this version, if the copy constructor throws the object will
 still be in a valid state.

 -> attribute inference for the generated opAssign:

 For version 1: opAssign attributes are inferred based on the copy
 constructor attrbiutes.
 For version 2: opAssign attributes are inferred based on copy
 constructor and destructor attributes
 For version 3: the declaration of the void array can be put
 inside a trusted block and then attributes are inferred based on
 copy constructor and destructor attributes

 If the copy constructor is marked `nothrow` and the struct
 defines a destructor, then version 2 is used, otherwise version 3.

 What are your thoughts on this?

 RazvanN
This all looks about right to me! I doubt there are any alternative options.

Jul 26 2018
parent RazvanN <razvan.nitu1305 gmail.com> writes:
 Why the memcpy?
 This looks inefficient.

 Is it in case the constructor throws?
 Have a 'nothrow' case where it constructs directly to this?
The copy constructor must be called on an object in the initial state, so it cannot be called directly on this as it is already initialized. __dtor is used as a matter of demonstration. Indeed, xdtor is the the alias which points to the generated destructor (__dtor, __fieldDtor or __aggregatedDtor)
Jul 27 2018
prev sibling next sibling parent Manu <turkeyman gmail.com> writes:
On Thu., 26 Jul. 2018, 2:45 am RazvanN via Digitalmars-d, <
digitalmars-d puremagic.com> wrote:

 void opAssign(ref $q1 S rhs)    // version 3
 {
      S tmp1 = rhs;                // call copy constructor
      void[S.sizeof] tmp2 = void;

      // swapbits(tmp1, this);
      memcpy(tmp2, this);
      memcpy(this, tmp1);
      memcpy(tmp1, tmp2);

      tmp1.__dtor();
 }
Uh oh, you feel for the trap! You can't destruct like this. __xdtor at least, but even then it's not so simple. I think emplace() should be lifted to druntime, and destroy() should be complemented by destruct(), which will not post-assign .init.
Jul 26 2018
prev sibling parent reply 12345swordy <alexanderheistermann gmail.com> writes:
On Thursday, 26 July 2018 at 09:40:03 UTC, RazvanN wrote:
 Hello everyone!

 As you probably know, I am working on the copy constructor DIP 
 and implementation. So far, I managed to implement 95% of the 
 copy constructor logic (as stated in the DIP). The point that 
 is a bit of a headache is how/when should opAssign be generated 
 when a copy constructor is defined. Now here is what I have 
 (big thanks to Andrei for all the ideas, suggestions and 
 brainstorms):

 [...]
Why are you not using the destroy() function? -Alexander
Jul 26 2018
parent Manu <turkeyman gmail.com> writes:
On Thu., 26 Jul. 2018, 9:35 am 12345swordy via Digitalmars-d, <
digitalmars-d puremagic.com> wrote:

 On Thursday, 26 July 2018 at 09:40:03 UTC, RazvanN wrote:
 Hello everyone!

 As you probably know, I am working on the copy constructor DIP
 and implementation. So far, I managed to implement 95% of the
 copy constructor logic (as stated in the DIP). The point that
 is a bit of a headache is how/when should opAssign be generated
 when a copy constructor is defined. Now here is what I have
 (big thanks to Andrei for all the ideas, suggestions and
 brainstorms):

 [...]
Why are you not using the destroy() function?
It pointlessly default initialises immediately after destruction. I tried to suggest destruct() function in complement when I was fiddling with it for C++ recently. I think it would be very useful.
Jul 27 2018