Skip to content

Design note: Postfix operators

Herb Sutter edited this page Aug 31, 2024 · 18 revisions

Q: Why are most Cpp2 unary operators postfix?

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 specifically, because of the Cpp2 design stake that "declaration follows use."

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

* and &

Consider this easy case:

f: (i:int) -> string = {/*...*/}

What is the type of f? of f(42)?

  • f is a function that takes an int and returns a string.
  • f(42) is a string.

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 an int and returns a pointer to a function that takes an int and returns a string. And we just said exactly that in code.
  • f(42) is type * (int) -> string, a pointer to a function that takes an int and returns a string.
  • f(42)* is type (int) -> string, a function that takes an int and returns a string.
  • f(42)*(1) is type string, a string.

Similarly, consider:

x: * int = /*...*/;

This also reads left to right:

  • x is type *int, a pointer to an int.
  • x* is type int, an int.

++ and --

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.

(no need for) ->

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.

The exceptions: What about !, -, and + ?

As already stated, I think it would be perfectly consistent to have all operators be postfix, because that follows the order of execution.

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 requiring not 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 overload operator! to spell invocations of that overload as not... and it would mean in Cpp2 we overload operator not which would be the only non-symbol that can follow an operator 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 writes 1- 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 as 1-, and 100+... 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.

Two examples from cppfront parse.h

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.

image

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.