-
Notifications
You must be signed in to change notification settings - Fork 246
Design note: Postfix operators
A: (1) Because it's WYSIWYG: The order in which we read and write the code is the same as the order of execution. (2) Additionally, for pointers and addresses specifically, because of the Cpp2 design stake that "declaration order and use order are consistent."
Note: This is the writeup that I promised in the CppCon 2022 talk.
Having the operators follow the order of execution means that consecutively executed operators are usually visually adjacent, and that Cpp2 expressions tend to need fewer parentheses.
Although this applies to all unary operators, I make an exception for !
, -
, and +
because of familiarity (see below).
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) -> * (j: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
.
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
.
I've made the case that it would be consistent to have all unary operators be postfix, because that follows the order of execution, and (for pointer/address operators) makes declaration order consistent with use order.
But (and this is a major usability "but"), there are two cases that are so common that they are hard to change, especially because changing them would not be solving a safety or simplicity problem with today's C++, and so would be arguably-gratuitous divergences from today's C++. The two cases are:
-
Logical unary
!
: Programmers in many languages are so used to writing!condition
(the "not" is before the condition). I do still sometimes consider removing!
and requiringnot
instead, and technically that would eliminate this case by making it not be a symbol, and so look less like an operator. But it would come with costs: It could create the problem that we have to teach programmers who use existing C++ types that overloadoperator!
to spell invocations of that overload asnot
... and it would mean in Cpp2 we overloadoperator not
which would be the only non-symbol that can follow anoperator
keyword, yet another inconsistency. It seems better to keep!
. -
Mathematical unary
-
and+
: Oven beyond programming languages, the notation-1
and+100
(unary plus/minus before the literal) is deeply entrenched in math (no mathematician I know writes1-
instead of-1
), and verbal communications (no human I know says "one minus" instead of "minus one"). I think it would be jarring to write them as1-
, and100+
... I never seriously considered doing that because it immediately reminds me of Reverse Polish Notation languages and calculators, and how despite many attempts they never really took off in the mainstream.
So I think there are strong usability arguments to make !
, -
, and +
be prefix operators. I don't think there are similar arguments for ~
or other operators, so those are postfix.
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 code, 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.