Skip to content

Design note: Postfix operators

Herb Sutter edited this page Oct 11, 2022 · 14 revisions

Q: Why are most Cpp2 operators postfix?

A: Because of the stake in the ground of "declaration follows use."

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.

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

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!

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

(not) ->

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.

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.