Generated opAssign in the presence of a copy constructor
RazvanN
razvan.nitu1305 at gmail.com
Thu Jul 26 09:40:03 UTC 2018
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
More information about the Digitalmars-d
mailing list