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. >