C++23’s new function syntax

We’ve had a couple of ways to spell functions for a long time:

[[nodiscard]] auto say_a_to(
    std::string_view what, std::string_view name) -> std::string {
  return std::string{what} + ", " + std::string{name} + "!";

say_a_to("Hi", "Kate"); // -> "Hi, Kate!"

struct {
  [[nodiscard]] auto operator()(
      std::string_view what, std::string_view name) const -> std::string {
    return std::string{what} + ", " + std::string{name} + "!";
} say_b_to;

say_b_to("Hello", "Tony"); // -> "Hello, Tony!"

And we’ve had a shortcut for that second one for a while, too:

auto say_c_to = [] [[nodiscard]] (
    std::string_view what, std::string_view name) -> std::string {
  return std::string{what} + ", " + std::string{name} + "!";

say_c_to("Bye", "Kate"); // -> "Bye, Kate!"

(Aside: notice where [[nodiscard]] is positioned, since C++23’s P2173, to apply to the lambda’s call operator.)

All these ways to spell functions look the same at the call site. But “regular functions” and function objects behave a bit differently, so as application and library writers we use them for different things according to our needs – how we need users to customize or overload them; whether we want ADL; etc.

C++23 added a new way to spell functions:

struct {
  [[nodiscard]] auto operator[](
      std::string_view what, std::string_view name) const -> std::string {
    return std::string{what} + ", " + std::string{name} + "!";
} say_d_to;

say_d_to["Goodbye", "Tony"]; // -> "Goodbye, Tony!"

It hasn’t been publicized as such, but that’s exactly what it is. C++20 removed the ability to use the comma operator in subscripts (P1161). As a follow-up to that, C++23 now allows multi-argument subscript operators (P2128). And that effectively gives us an alternative syntax for calling functions. The subscript operator now has the same mechanics as the function call operator. These operators are now the only two that can take arbitrary numbers of arguments of arbitrary types. And they’re also the same precedence.

So we can do things like this too:

struct {
  template <std::integral ...Ts>
  [[nodiscard]] auto operator[](Ts... ts) const noexcept {
    return (0 + ... + ts);
} sum;

And call it accordingly:

const auto s1 = sum[1, 2, 3]; // 6
const auto s2 = sum[];        // 0

(Yes, operator[] can also be written as a nullary function.)

This works, today. Probably because to the compiler this was already the bread-and-butter of how functions work anyway, so I’m guessing (although IANACW) it was pretty easy to implement these papers.

P2128‘s envisaged use cases are all about numeric computing and multi-dimensional arrays with integral subscripting. But that’s not all that operator[] is now. Quite literally, it’s alternative syntax for a function call, with everything that might imply.

What use might this be? Well a few things spring to mind. Using operator[] for function calls has all the same lookup and customization implications as using operator(), but adds the inability to call through type-erased function wrappers — at least at the moment. So that might be useful to someone.

A second convention that springs to mind is perhaps for pure functions. If a function is “pure” then it will always return the same output given the same input, which means mathematically it can be implemented with a lookup table. Using operator[] historically looks something like a map lookup, so perhaps it’s a natural fit for pure function syntax?

It might also be useful to naturally express two different areas of functionality within a library, or operations with different evaluation semantics (compile-time? runtime? lazy?) as characterized by function calls with operator() and with operator[]. This would perhaps provide a nice call-site indication to make the code more readable.

There are sure to be other uses. Should you look for operator[] coming soon to a library near you? I don’t know. This might seem strange to some folks, but it’s not necessarily less readable; just less familiar. And if there’s one thing I know about C++, it’s that it’s a short hop from bizarre newly-discovered quirk to established technique. operator[] is now equivalent to operator(), and when someone finds a use for that, it will get used.

Categorized as C++