Skip to content

Another round of docs updates #983

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions docs/cpp2/metafunctions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The most important thing about metafunctions is that they are not hardwired lang

## Applying metafunctions

Metafunctions provide an easy way for a type author to opt into a group of defaults, constraints, and generated functions: Just write `@name` afer the `:` of a declaration, where `name` is the name of the metafunctions. This lets the type author declare (and the human reader see) the intent up front: "This isn't just any `type`, this is a `@value type`" which automatically gives the type default/copy/move construction and assignment, `<=>` with `std::strong_ordering` comparisons, and guarantees that it has a public destructor and no protected or virtual functions:
Metafunctions provide an easy way for a type author to opt into a group of defaults, constraints, and generated functions: Just write `@name` afer the `:` of a declaration, where `name` is the name of the metafunction. This lets the type author declare (and the human reader see) the intent up front: "This isn't just any `type`, this is a `@value type`" which automatically gives the type default/copy/move construction and assignment, `<=>` with `std::strong_ordering` comparisons, and guarantees that it has a public destructor and no protected or virtual functions:

``` cpp title="Example: Using the value metafunction when writing a type"
point2d: @value type = {
Expand Down Expand Up @@ -95,7 +95,7 @@ skat_game: @enum<i16> type = {

Consider `hearts`: It's a member object declaration, but it doesn't have a type (or a default value) which is normally illegal, but here it's okay because the `@enum<i16>` metafunction fills them in: It iterates over all the data members and gives each one the underlying type (here explicitly specified as `i16`, otherwise it would be computed as the smallest signed type that's big enough), and an initializer (by default one higher than the previous enumerator).

Unlike C `enum`, this `@enum` is scoped and strongly typed (does not implicitly convert to the underlying type.
Unlike C `enum`, this `@enum` is scoped and strongly typed (does not implicitly convert to the underlying type).

Unlike C++11 `enum class`, it's "just a `type`" which means it can naturally also have member functions and other things that a type can have:

Expand Down Expand Up @@ -141,10 +141,11 @@ file_attributes: @flag_enum<u8> type = {
// name_or_number is declaratively a safe union/variant type:
// it has a discriminant that enforces only one alternative
// can be active at a time, members always have a name, and
// each member has .is_member() and .member() accessors...
// the word "union" carries all that meaning as a convenient
// and readable opt-in without hardwiring "union" specially
// into the language
// each member has .is_member(), .set_member(), and .member()
// accessors using the member name... the word "union"
// carries all that meaning as a convenient and readable
// opt-in without hardwiring "union" specially into the
// language
//
name_or_number: @union type = {
name: std::string;
Expand All @@ -155,6 +156,7 @@ main: () = {
x: name_or_number = ();

x.set_name("xyzzy"); // now x is a string
assert( x.is_name() );
std::cout << x.name(); // prints the string

// trying to use x.num() here would cause a Type safety
Expand Down
29 changes: 18 additions & 11 deletions docs/cpp2/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ A `this` parameter of an `operator=` function can additionally be declared as:

## `operator=` — Construction, assignment, and destruction

All value operations are spelled `operator=`, including construction, assignment, and destruction. `operator=` sets the value of `this` object, so the `this` parameter can be pass as anything but `in` (which would imply `const`):
All value operations are spelled `operator=`, including construction, assignment, and destruction. `operator=` sets the value of `this` object, so the `this` parameter can be passed as 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.

Expand All @@ -120,11 +120,14 @@ All value operations are spelled `operator=`, including construction, assignment

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.

TODO Return type of assignment operator?

> Note: 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 `=` in Cpp2.


### `that` — A source parameter

All functions can have a **`that`** is a synonym for the object to be copied/moved from. Like `this`, at type scope it is never declared with an explicit `: its_type` because its type is always the current type.
All type-scope functions can have **`that`** as their second parameter, which is a synonym for the object to be copied/moved from. Like `this`, at type scope it is never declared with an explicit `: its_type` because its type is always the current type. Unlike `this`, `that` is always passed as a `in` (the default) or `move` parameter.

`that` can be an `in` (default) or `move` parameter. Which you choose naturally determines what kind of member function is being declared:

Expand Down Expand Up @@ -157,7 +160,7 @@ In Cpp1 terms, they can be described as follows:

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

Expand All @@ -169,21 +172,23 @@ There are only two defaults the language will generate implicitly for a type:

- The only special function every type must have is the destructor. If you don't write it by hand, a public nonvirtual destructor is generated by default.

- If no `operator=` functions are written by hand, a public default constructor is generated by default.
- If no `operator=` functions other than the destructor are written by hand, a public default constructor is generated by default.

All other `operator=` functions are explicitly written, either by hand or by opting into applying a metafunction (see below).

> Note: Because generated functions are always opt-in, you can never get a generated function that's wrong for your type, and so Cpp2 doesn’t need to support "=delete" for the purpose of suppressing unwanted generated functions.

### Memberwise by default

All copy/move/comparison `operator=` functions are memberwise by default in Cpp2. That includes when you write memberwise construction and assignment yourself. In a hand-written `operator=`:
All copy/move/comparison `operator=` functions are memberwise by default in Cpp2. That includes when you write memberwise construction and assignment yourself.

- The body must begin with a series of `member = value;` statements, one for each of the type's data members in order.
In a hand-written `operator=`:

- If the body does not mention a member, by default the member's default initializer is used.
- The body must begin with a series of `member = value;` statements, one for each of the type's data members (including base classes) in declaration order.

- In an assignment operator (`inout this`), you an explicitly skip setting a member by writing `member = _;` where it would normally be set, if you know you have a reason to set its value later instead.
- If the body does not mention a member in the appropriate place in the beginning section, by default the member's default initializer is used.

- In an assignment operator (`inout this`), you can explicitly skip setting a member by writing `member = _;` where it would normally be set if you know you have a reason to set its value later instead or if the existing value needs to be preserved.

For example:

Expand Down Expand Up @@ -213,6 +218,8 @@ mytype: type
print();
}

// TODO skipping the default memberwise assignment for later setting

print: (this) = std::cout << "value is [(name)$] [(social_handle)$]\n";
}

Expand All @@ -224,12 +231,12 @@ main: () = {
y = x; // copy assign
z := (move x); // move construct
z = (move y); // move assign
x.print(); // [] [] - moved from
y.print(); // [] [] - moved from
x.print(); // "value is [] []" - moved from
y.print(); // "value is [] []" - 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.
> 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 if it's a conversion function (non-`that`, aka non-copy/move), and using memberwise `member = that.member;` for copy/move functions.



Expand Down