-
Notifications
You must be signed in to change notification settings - Fork 263
Cpp2: operator=, this & that
this
is explicitoperator=
is the name for all value-setting functions: construction, assignment, conversion, and destructionthat
is explicit, used for copy/moveoperator=
can generalize (A)ssignment from construction, and (M)ove from copyoperator=
always defaults to memberwise- misc. notes
this
is a synonym for the current object. Inside the scope of a type that has a member named member
, member
by default means this.member
.
Note: In Cpp2,
this
is not a pointer.
The name this
may only be used for the first parameter of a type-scope function (aka member function). It is never declared with an explicit : its_type
because its type is always the current type.
this
can be an in
(default), inout
, out
, or move
parameter. Which you choose naturally determines what kind of member function is being declared:
-
in this
: Writingmyfunc: (this /*...*/)
, which is shorthand formyfunc: (in this /*...*/)
, defines a Cpp1const
-qualified member function, becausein
parameters areconst
. -
inout this
: Writingmyfunc: (inout this /*...*/)
defines a Cpp1 non-const
member function. -
out this
: Writingmyfunc: (out this /*...*/)
defines a Cpp1 constructor... and more. (See below.) -
move this
: Writingmyfunc: (move this /*...*/)
defines a Cpp1&&
-qualified member function, or if there are no additional parameters it defines the destructor.
A this
parameter can additionally be declared as one of the following:
-
virtual
: Writingmyfunc: (virtual this /*...*/)
defines a new virtual function. -
override
: Writingmyfunc: (override this /*...*/)
defines an override of an existing base class virtual function. -
final
: Writingmyfunc: (final this /*...*/)
defines a final override of an existing base class virtual function. -
implicit
: On a two-parameteroperator=
function, writingoperator=: (implicit out this, /*...*/)
defines a conversion function that can be used to perform implicit conversions to this type. Note thatimplicit
only has an effect when theoperator=
function is used as a default or converting constructor (or is a conversion operator) -- the option governs whether this function can be used to implicitly generate a temporary object of this type, which is something only a constructor or conversion operator can do, an assignment operator does not.
operator=
is the name for all value-setting functions: construction, assignment, conversion, and destruction
operator=
is the generalized name for all value-setting functions, including the six combinations of { copy, move, converting } x { constructors, assignment operators }.
Note: The concept of a converting assignment operator is first-class in Cpp2, and makes this 3x2 matrix of options symmetric. In Cpp1, it's common to write a converting constructor from some other type, but not an assignment operator from that other type, which leads to asymmetries like
mytype var = other;
working butvar = other;
not working.
operator=
is always opt-in. If you don't ask for one (by writing it explicitly or asking for it via a metaclass function), you won't get one.
Note: This means you never need to "=delete" a compiler-generated special member function that got generated automatically but that you didn't want. You only get the ones you ask for, but asking for them is more convenient and safe in Cpp2.
operator=
sets the value of this
object, so the this
parameter can be anything but in
(which would imply const
):
-
out this
: Writingoperator=: (out this /*...*/ )
is naturally both a constructor and an assignment operator, because anout
parameter can take an uninitialized or initialized argument. If you don't write a more-specializedinout this
assignment operator, Cpp2 will use theout this
function also for assignment. -
inout this
: Writingoperator=: (inout this /*...*/ )
is an assignment operator (only), because aninout
parameter requires an initialized modifiable argument. -
move this
: Writingoperator=: (move this)
is the destructor. No other parameters are allowed, so it connotes "movethis
nowhere."
that
is a synonym for the object to be copied/moved from.
The name that
may only be used for the second parameter of a type-scope operator=
function. Like this
, it is never declared with an explicit : its_type
because its type is always the current type.
that
can be an in
(default) or move
parameter. Which you choose naturally determines what kind of member function is being declared:
-
in that
: Writingmyfunc: (/*...*/ this, that)
, which is shorthand formyfunc: (/*...*/ this, in that)
, is naturally both a copy and move function, because it can accept an lvalue or an rvalue argument. If you don't write a more-specializedmove that
move function, Cpp2 will automatically use thein that
function also for move. -
move that
: Writingmyfunc: (/*...*/ this, move that)
defines a move function.
As mentioned above:
- If you don't write an
inout this
function, Cpp2 will use yourout this
function in its place (if you wrote one). - If you don't write a
move that
function, Cpp2 will use yourin that
function in its place (if you wrote one).
Note: When lowering to Cpp1, this just means generating the applicable special member functions from the appropriate Cpp2 function.
This graphic summarizes these generalizations. For convenience I've numbered the (A)ssignment and (M)ove defaults.
In Cpp1 terms, they can be described as follows:
-
(M)ove, M1, M2: If you write a copy constructor or assignment operator, but not a corresponding move constructor or assignment operator, the latter is generated.
-
(A)ssignment, A1, A2, A3: If you write a copy or move or converting constructor, but not a corresponding copy or move or converting assignment operator, the latter is generated.
-
The arrows are transitive. For example, if you write a copy constructor and nothing else, the move constructor, copy assignment operator, and move assignment operator are generated.
-
M2 is preferred over A2. Both M2 and A2 can generate a missing
(inout this, move that)
function. If both options are available, Cpp2 prefers to use M2 (generate move assignment from copy assignment, which could itself have been generated from copy construction) rather than A2 (generate move assignment from move construction). This is because M2 is a better fit: Move assignment is more like copy assignment than like move construction, because assignments are designed structurally to set the value of an existingthis
object.
The most general operator=
with that
is (out this, that)
. In Cpp1 terms, it generates all four combinations of { copy, move } x { constructor, assignment }. This is often sufficient, so you can write all these value-setting just once. If you do want to write a more specific version that does something else, though, you can always write it too.
Note: Generating
inout this
(assignment) fromout this
also generates converting assignment from converting construction, which is a new thing. Today in Cpp1, if you write a converting constructor from another typeX
, you may or may not write the corresponding assignment fromX
; in Cpp2 you will get that by default, and it sets the object to the same state as the converting constructor fromX
does.
operator=
always defaults to memberwise semantics. The body of the operator must begin with a series of member = value;
statements, one for each of the type's data members in order. If the body skips a member, the member's default initializer is used. For example:
mytype: type =
{
// data members (private by default)
name: std::string;
social_handle: std::string = "(unknown)";
// conversion from string
operator=: (out this, who: std::string) = {
name = who;
// if social_handle is not mentioned, defaults to:
// social_handle = "(unknown)";
// now that the members have been set,
// any other code can follow...
print();
}
// copy/move constructor/assignment
operator=: (out this, that) = {
// if neither data member is mentioned, defaults to:
// name = that.name;
// social_handle = that.social_handle;
print();
}
print: (this) = std::cout << "value is [(name)$] [(social_handle)$]\n";
}
// The above definition of mytype allows all of the following...
main: () = {
x: mytype = "Jim"; // construct from string
x = "John"; // assign from string
y := x; // copy construct
y = x; // copy assign
z := (move x); // move construct
z = (move y); // move assign
x.print(); // [] [] - moved from
y.print(); // [] [] - moved from
}
Note: This makes memberwise semantics symmetric for construction and assignment. In Cpp1, only non-copy/move constructors have a default, which is to initialize a member with its default initializer. In Cpp2, both constructors and assignment operators default to using the default initializer for if it's a conversion function (non-
that
, aka non-copy/move), and using memberwisemember = that.member;
for copy/move functions.
Unifying operator=
enables usable out
parameters, which is essential for composable guaranteed initialization. We want the expression syntax x = value
to be able to call a constructor or an assignment operator, so naming them both operator=
is consistent.
More generally, writing =
always invokes an operator=
(in fact for a Cpp2-authored type, and semantically for a Cpp1-authored type). This avoids the Cpp1 inconsistency that "writing =
calls operator=
, except when it doesn't" (such as in a Cpp1 variable initialization). Conversely, operator=
is always invoked by =
.