Skip to content

Design note: Capture

Herb Sutter edited this page Jun 28, 2023 · 2 revisions

Q: Why use postfix $ for capture? Wouldn't { } be nicer for string interpolation, like Python?

A: Postfix $ is clear and unambiguous, and works well in all the places I want to allow capture in the language, not just string interpolation.

(See also the ~3 minutes of the CppCon 2022 talk starting at 1:30:53.)

I'm open to changing this syntax, but I view it as essential to aim for a single consistent capture syntax that works the same in all the places we do capture, where "capture" means "take an expression in this context and store a copy of its current value for use later."

Here are four places this comes up, the first three of which are currently implemented in cppfront:

  1. Local functions (aka lambdas): :(i:_) = i + offset$; -- capture a copy of offset on closure creation for use later each time the closure is invoked

  2. Postconditions: [[post: v.size() == v.size()$ + 1]] -- capture a copy of v.size() on function entry for use later when the whole postcondition is evaluated on function exit

  3. String interpolation: "My name is (name)$" -- capture a copy of name on string construction for use later when the string value is referred to

Note: I realize this interpolation syntax is the reverse order of most other languages which put the "paste" character first. JavaScript would be "My name is $(name)". Swift would be "My name is \(name)". Python would be "My name is {name}".

  1. And when we get to reflection and generation (e.g., metaclass functions), I want to be able to generate code by pasting compile-time values into generated run-time code which in my model is also a textual subtitution (e.g., something like reflecting on a function to be wrapped and storing the reflection in a compile-time variable named func, and then generating (func.name()+"_wrapper")$: (forward func.params.first().name$: _) = { do_wrapped_extra_stuff(); func.name()$(func.params.first().name$); } } as a new function -- we capture copies of of func's attributes at the time we generate the code for use later when the code is compiled)

I'm not opposed to making the $ a prefix, or switching to { }. Some side by side examples:

  1. Local functions (aka lambdas):
  • :(i:_) = i + offset$;
  • :(i:_) = i + $offset;
  • :(i:_) = i + {offset};
  1. Postconditions:
  • [[post: v.size() == v.size()$ + 1]]
  • [[post: v.size() == $(v.size()) + 1]] (this or other examples will likely want parens for actual precedence or visual uncertainty)
  • [[post: v.size() == {v.size()} + 1]]
  1. String interpolation:
  • "My name is (name)$"
  • "My name is $(name)"
  • "My name is {name}"
  1. Reflection and generation:
  • (func.name()+"_wrapper")$: (forward func.params.first().name$: _) = { do_wrapped_extra_stuff(); func.name()$(func.params.first().name$); } }
  • $(func.name()+"_wrapper"): (forward $(func.params.first().name): _) = { do_wrapped_extra_stuff(); $(func.name())($(func.params.first().name)); } }
  • {func.name()+"_wrapper"}: (forward {func.params.first().name}: _) = { do_wrapped_extra_stuff(); {func.name()}({func.params.first().name}); } }

For switching to prefix $, a minor concern would be that it will generally mean needing more parens in the usual kinds of expressions places where they're needed today for prefix operators.

For switching to just { }, two things I considered are:

  • (Major) To me, the absence of $ makes most of these examples harder to read. I think that having the $ makes it clearer that there's a special "paste" operation going on.
  • (Minor, IMO) For string interpolation there could be an advantage that { and } are probably less likely to appear in strings and therefore need escaping less often than $ (which could be a currency symbol in some regions). To me this feels like a weaker argument because some escaping will be necessary no matter what syntax is chosen, and all of these are relatively uncommon symbols.

To consider changing to the capture syntax I'd like to have some additional evidence, including experience actually trying out this syntax and learning whether there's a problem that needs to be solved, or experience from other languages that a given syntax really didn't work well for them (e.g., did using $ cause known interference with other tools).