Skip to content

Cpp2: operator=, this & that

Herb Sutter edited this page Mar 20, 2023 · 6 revisions

operator=, this & that

Contents

  • this is explicit
  • operator= is the name for all value-setting functions: construction, assignment, conversion, and destruction
  • that is explicit, used for copy/move
  • operator= can generalize (A)ssignment from construction, and (M)ove from copy
  • operator= always defaults to memberwise
  • misc. notes

this is explicit

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: Writing myfunc: (this /*...*/), which is shorthand for myfunc: (in this /*...*/), defines a Cpp1 const-qualified member function, because in parameters are const.
  • inout this: Writing myfunc: (inout this /*...*/) defines a Cpp1 non-const member function.
  • out this: Writing myfunc: (out this /*...*/) defines a Cpp1 constructor... and more. (See below.)
  • move this: Writing myfunc: (move this /*...*/) defines a Cpp1 &&-qualified member function, or if there are no additional parameters is defines the destructor.

A this parameter can additionally be declared as one of the following:

  • virtual: Writing myfunc: (virtual this /*...*/) defines a new virtual function.
  • override: Writing myfunc: (override this /*...*/) defines an override of an existing base class virtual function.
  • final: Writing myfunc: (final this /*...*/) defines a final override of an existing base class virtual function.
  • implicit: On a two-parameter operator= function, writing operator=: (implicit out this, /*...*/) defines a conversion function that can be used to perform implicit conversions to this type.

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 but var = 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: Writing operator=: (out this /*...*/ ) is naturally both a constructor and an assignment operator, because an out parameter can take an uninitialized or initialized argument. If you don't write a more-specialized inout this assignment operator, Cpp2 will use the out this function also for assignment.
  • inout this: Writing operator=: (inout this /*...*/ ) is an assignment operator (only), because an inout parameter requires an initialized modifiable argument.
  • move this: Writing operator=: (move this) is the destructor. No other parameters are allowed, so it connotes "move this nowhere."

that is explicit, used for copy/move

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: Writing myfunc: (/*...*/ this, that), which is shorthand for myfunc: (/*...*/ 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-specialized move that move function, Cpp2 will automatically use the in that function also for move.
  • move that: Writing myfunc: (/*...*/ this, move that) defines a move function.

operator= can generalize (A)ssignment from construction, and (M)ove from copy

As mentioned above:

  • If you don't write an inout this function, Cpp2 will use your out this function in its place (if you wrote one).
  • If you don't write a move that function, Cpp2 will use your in 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.

image

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 existing this 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) from out this also generates converting assignment from converting construction, which is a new thing. Today in Cpp1, if you write a converting constructor from another type X, you may or may not write the corresponding assignment from X; in Cpp2 you will get that by default, and it sets the object to the same state as the converting constructor from X does.

operator= always defaults to memberwise

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 memberwise member = that.member; for copy/move functions.

misc. notes

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

Clone this wiki locally