-
Notifications
You must be signed in to change notification settings - Fork 251
Design note: Postfix operators
Note: This is the writeup that I promised in the CppCon 2022 talk.
This applies to nearly all unary operators (except !
, -
, and +
which I'll argue naturally want to be prefix, because of the familiarity of !condition
, -1
, and +100
.
Consider this easy case:
f: (i:int) -> string = {/*...*/}
What is the type of f
? of f(42)
?
-
f
is a function that takes anint
and returns astring
. -
f(42)
is astring
.
That was a good warmup. Now consider this function:
f: (i:int) -> * (:int) -> string = {/*...*/}
This reads left to right:
-
f
is type(int) -> * (int) -> string
, a function that takes anint
and returns a pointer to a function that takes anint
and returns astring
. And we just said exactly that in code. -
f(42)
is type* (int) -> string
, a pointer to a function that takes anint
and returns astring
. -
f(42)*
is type(int) -> string
, a function that takes anint
and returns astring
. -
f(42)*(1)
is typestring
, astring
.
Similarly, consider:
x: * int = /*...*/;
This also reads left to right:
-
x
is type*int
, a pointer to anint
. -
x*
is typeint
, anint
.
In Cpp2, my current experiment to disambiguate postfix unary operators that look similar to binary operators is that the postfix unary operators have to be written without intervening whitespace, such as x*
. And that's pretty much the only rule to remember... additionally, as a convenience, cases like x**y
mean the same as x* * y
since they can't mean anything else, which is convenient so we can still write things like a*b
and a*2
conveniently with the obvious meaning. That's the current experiment!
Similarly, ++
and --
are postfix-only, with in-place semantics. If we want the old value, we know how to keep a copy! This way I aim to never have to have another neverending "when is prefix vs postfix increment better" comment thread, and to never have to remember (or teach) the "dummy (int)
parameter" quirk when overloading these functions.
When you have postfix *
, there's no need for a separate ->
operator, because that is naturally spelled *.
. And in fact this just embraces what has already been true since the 1970s in C... for built-in types, a->b
already means (*a).b
, and now we can write it without the parens as simply a*.b
.
To illustrate, here are two snippets from the cppfront compiler itself. On the left is how the code is written today in Cpp1 syntax, and on the right is how I want to be able to write the code in Cpp2 syntax.
A request: As a quick exercise, please:
- Take 30 seconds to just read the right-hand side, and reason about what each highlighted expression means.
- Then consider the left-hand side, and remember that all the prefix operators actually apply (unless parenthesized) to the most distant thing that's furthest away(!).
- Finally, consider the right-hand side again, and think about why you don't need parentheses, and how this fits with consistently left-to-right function chaining.