An Infix Operator and a Keyword Escape for C++

Two jobs for the backtick, the last free token

Document #: P4307R0 [Latest] [Status]
Date: 2026-09-22
Project: Programming Language C++
Audience: EWG
Reply-to: Steve Downey
<>

Contents

1 Abstract

We propose two uses for one character: the backtick, the last printable ASCII character the language can still usefully claim. One makes any callable a binary operator; the other lets a keyword be escaped for use as an ordinary identifier. Each is independently motivated, and each is defined by rewrite into something the language already has. They are proposed together, with equal standing, because they share the token; two independent claims on one character, designed separately, end in contradiction.

The infix operator. C++ has two kinds of binary operation. A fixed set — the ones with tokens — may be written between their operands: a + b, a < b, a | b. Every other binary operation, every operation with a name, is a prefix call: gcd(m, n), dot(u, v), intersects(a, b). The distinction is lexical accident and not design. We propose to erase it at the call site. Put a callable between backticks and it is a binary operator:

a `plus` b              // plus(a, b)
a `std::min` b          // std::min(a, b)
u `dot` v               // dot(u, v)
a `f` b `g` c           // g(f(a, b), c)   — left-associative

x `f` y is defined to be f(x, y), where the text between the backticks is an arbitrary call-eligible expression. The construct is borrowed from Haskell (`div`, `mod`, `elem`), and it desugars in the front end to the ordinary call, before overload resolution runs. It therefore inherits overload resolution, ADL, templates and SFINAE, constexpr evaluation, conversions, value categories, and code generation, rather than restating any of them. There is no new overloadable operator`, no operator registry, no fixity declarations. The feature has essentially no semantics of its own to get wrong.

The keyword escape. C++ has no way to use a keyword as a name, so every keyword the committee adds breaks every program that used the word as one. The committee knows this, and pays for it every time: C++20 shipped co_await, co_yield, and co_return because await and yield were taken, and made module and import context-sensitive — at real specification and implementation cost — because breaking existing code was not acceptable. The languages designed since have built in an escape for this case: Swift’s `class`, Kotlin’s backtick identifiers, F#’s double-backtick names, Rust’s r# raw identifiers. We propose the same hatch. A backtick pair in name position escapes a word and yields a plain identifier, so lookup, mangling, linkage, and ABI are untouched; void `new`(); declares an ordinary function named new. The word need not be a keyword, which makes the hatch prospective rather than merely remedial: `foobar` is the same identifier as foobar, so a name can be escaped before the committee takes it, in code that still has to compile today. Future keywords stop being breaking changes, and stop needing co_-style circumlocution to avoid becoming one.

The two uses never collide: they occupy mutually exclusive grammatical positions, the same position-based disambiguation the language already applies to *, &, and <. Both are implemented, gated behind an opt-in -fbacktick flag, in two independent compilers, public forks of Clang and GCC, with tests. This is a pure core-language proposal targeting C++29. No library additions are proposed.

2 Before / After

2.1 The infix operator

Before
After
if (approx_equal(dot(u, v), 0.0))
    reject(u, v);
if (u `dot` v `approx_equal` 0.0)
    reject(u, v);
auto d = std::gcd(std::gcd(a, b), c);
auto d = a `std::gcd` b `std::gcd` c;
if (intersects(a, b) && !contains(a, b))
    handle_partial_overlap(a, b);
if (a `intersects` b && !(a `contains` b))
    handle_partial_overlap(a, b);
// evaluation inverts reading order:
// innermost call is the first stage
store(validate(parse(s)));
// with a two-line pipe helper,
// stages read left to right
s `pipe` parse `pipe` validate `pipe` store;

The prefix spelling nests where the infix spelling chains. dot then approx_equal is the order the reader thinks in, and the order the chain reads in; the call spelling is inside-out. None of the “after” column is new semantics; each right-hand side is defined as the call on its left. The pipe helper in the last row is two lines of ordinary user code (pipe(x, f) returns f(x)), shown as motivation. It is not part of the proposal.

2.2 The keyword escape

Before
After
// C++17: fine.  C++20: ill-formed —
// 'requires' became a keyword.
bool requires(const License& l);

if (requires(user.license))
    admit(user);
// declares and calls an ordinary
// function whose name is "requires"
bool `requires`(const License& l);

if (`requires`(user.license))
    admit(user);

Today the “before” column has no fix short of renaming the function and every use of it, across every translation unit and every client. With the escape, the declaration and its call sites take a backtick pair and nothing else changes: same name, same mangling, same ABI. The deeper payoff is prospective. A committee choosing a future keyword no longer has to weigh breaking every use of a good name against mangling the keyword into co_-style circumlocution or context-sensitivity. The escape hatch makes clean keywords affordable.

That payoff depends on the escape wrapping any identifier, including the words that are not keywords yet. Suppose the escape is standardized in one revision and the committee takes requires in the next. If only a keyword could be escaped, the After column would be ill-formed in the first revision, where requires is still an ordinary name, and mandatory in the second. A header that must compile as both would need two spellings and a macro to choose between them, and no name could be escaped until after it had broken. A precaution that cannot be taken until after the damage is a repair. Because any identifier may be escaped, the After column declares the same function in both revisions, and code can be escaped in advance, on purpose, before the committee has chosen anything.

This is the rule regular expressions already use. A backslash before a metacharacter means that character, and a backslash before a character that was never a metacharacter is just that character, which is why a generator can escape unconditionally instead of consulting a table and why a person can future-proof a pattern by escaping on sight. The backtick escape is the same construct one level up. It suppresses syntax when the word is syntax, and when the word is not syntax it is the word.

3 The proposal

Two features, one token, one paper.

The infix operator. x `f` y is sugar for f(x, y) (equivalently (f)(x, y)), where the operator slot between the backticks is an arbitrary expression parsed as an assignment-expression, or a type-name, which constructs. The value, type, and semantics are exactly those of the corresponding call. The operator is left-associative and binds tighter than any other binary operator, looser than the unary and postfix operators.

The keyword escape. In any position where the grammar expects a name (a declarator-id, a class, enumeration or namespace name, a template parameter’s name, a label, an operand, after ., ->, or ::), a backtick pair wrapping a word spelled as an identifier denotes an ordinary identifier whose spelling is that word. Keywords are the case the feature is named for, and the rule is not restricted to them: `foobar` and foobar are the same identifier, so a declaration may use one spelling and its uses the other. It is purely a source-level construct; the resulting identifier participates in lookup, mangling, and linkage exactly as if the word had never been a keyword.

Both are gated during the proposal period behind a compiler flag; a standardized form drops the gate. Flag off, every existing valid program is untouched: backtick remains, as today, a character with no meaning outside literals.

4 Motivation, beyond readability

The Before/After tables carry the shallow case: binary operations read better infix, and named operations stop being visually second-class to the dozen built-in symbols. The deeper case rests on two properties the tables barely use. The operator slot is an arbitrary callable expression, and the operator chains left-associatively. Those two facts reach further than they first appear. Everything in this section is the operator plus a few lines of ordinary user code; none of it is proposed for the standard library (the paper is language-only), and all of it was compiled and run under the built Clang.

4.1 Short-circuiting returns to user code

C++ reserves non-strict evaluation to a fixed set of built-ins: &&, || and ?:. The comma operator is the near neighbor, and is not one of them: it evaluates both of its operands, and what it fixes is the sequencing. Overloading does not get non-strict evaluation back: an overloaded operator&& evaluates both operands, which is the trap everyone has been bitten by, and the reason the standing advice is: don’t. This is a capability boundary, not a style preference, and it has been closed since C++98.

A backtick helper whose right operand is a callable reopens it, because the helper decides whether (and when) that operand runs:

// short-circuiting logical implication:  p => q  ≡  !p || q
inline constexpr auto implies =
    [](bool p, auto&& q) -> bool { return !p || q(); };

p `implies` [&]{ return expensive(); }   // runs only when p holds

The same shape gives lazy defaults (opt `or_else` [&]{ return costly(); }), guarded effects, and bespoke control operators. Any binary operation that must not evaluate its right side unconditionally is in reach. Chaining fallible steps is the zero-ceremony special case: the stages are already functions, so no thunk is written and the short-circuit falls out:

inline constexpr auto mbind =
    [](auto&& m, auto&& f)
    { return std::forward<decltype(m)>(m)
                 .and_then(std::forward<decltype(f)>(f)); };

parse(s) `mbind` validate `mbind` store;   // stops at the first error

The concession: a bare-expression right operand still evaluates eagerly, because backtick desugars to a call and calls, of course, evaluate their arguments. The thunk is the price of generality. A dedicated implication operator pays that price differently, by building the laziness into the operator; Walter Brown has proposed operator=>, with short-circuit evaluation like && and || [P2971R3]. However, what it buys over the backtick spelling is ergonomic: omitting the thunk in the common boolean case. The capability is already there.

4.2 One helper makes it a pipeline, and the ranges closures already fit

A two-line helper turns the operator into left-to-right value threading:

inline constexpr auto pipe =
    [](auto&& x, auto&& f) -> decltype(auto)
    { return std::invoke(std::forward<decltype(f)>(f),
                         std::forward<decltype(x)>(x)); };

x `pipe` f `pipe` g `pipe` h        // h(g(f(x))) — data-flow order

The decisive case is that this drives the existing range adaptor closures unchanged. views::filter(pred) and views::transform(fn) are already unary callables (c | a is defined as a(c)), so pipe feeds them directly:

r `pipe` views::filter(pred) `pipe` views::transform(fn)
// identical result and laziness to:
r |  views::filter(pred) |  views::transform(fn)

Same closure objects, same lazy views, verified identical output on the built compiler. The per-library operator| overloads exist only to choose the | syntax; the closures themselves need nothing, so they work under backtick for free.

4.3 Stages with extra arguments: the honest gap

A free function that takes the threaded value first plus extra arguments needs its trailing arguments fixed, with std::bind_back (C++23) or a lambda, before it is a unary stage:

r `pipe` std::bind_back(filter, pred) `pipe` std::bind_back(transform, fn)
// == transform(filter(r, pred), fn)

However, this is the case P2011’s |> writes more directly, with the arguments inline. Same outcome, more ceremony, and it is why backtick does not make |> redundant. The boundary is drawn in the next section.

4.4 Reusable, point-free composition

Compose stages into a named pipeline once, apply it many times:

inline constexpr auto then =
    [](auto f, auto g)
    { return [=](auto&&... a) -> decltype(auto)
        { return g(f(std::forward<decltype(a)>(a)...)); }; };

auto clean = trim `then` lower `then` dedup;   // a reusable callable
clean(s);

Left association gives left-to-right composition, mirroring a reusable adaptor chain.

4.5 Motivation, not a library proposal

None of pipe, then, mbind, or implies is proposed. Each is a few lines of user code the operator makes worth writing; standardizing them would add an LEWG track to an EWG/CWG paper and double the committee cost for no enabling gain. Land the language feature, let usage show which helpers deserve a place, and bring those in a companion library paper with field experience behind them rather than ahead. The patterns are here so the room can see the operator’s reach before approving any of it. Direction without commitment.

5 Neither replaces the other: P2011’s |>

The obvious neighbor is the pipeline-rewrite operator, |> [P2011R1]. The comparison below is against that 2020 design, which states the rewrite most plainly; the discussion has since moved on to P2672R0 [P2672R0], which reopens the design space and puts several shapes of the operator in front of EWG. Nothing here turns on which shape wins: the orthogonality is between a symmetric binary infix operator and a directional rewrite that prepends the left operand to a call, and every shape under discussion is the latter. Both constructs bottom out in a call expression, and their two-argument cases coincide (a `plus` b, a |> plus(b), and plus(a, b) are the same call). They are orthogonal and complementary, and neither subsumes the other.

What each one is:

backtick x `f` y
pipeline x |> f(...)
Shape symmetric binary infix directional prepend-the-argument
Right-hand side a single operand a call with its own arguments
Resulting arity exactly 2 any N
Mechanism desugar to a normal call pure syntactic rewrite
Overloadable yes; it is a call no, by design
Precedence highest binary low
Native use operations: a `min` b chains: r |> filter(p) |> sum()

The overlap stops at two arguments. Beyond that, each can do what the other cannot:

And they compose: backtick supplies infix detail inside a stage, |> threads the value between stages:

r |> filter([](auto e){ return e `mod` 2 `eq` 0; }) |> sum()
//                             \____ eq(mod(e, 2), 0) ____/

This proposal declines the |> spelling for itself (see the appendix) so that both can coexist in one program. Backtick says “this is a binary operation”; |> says “thread this value through these stages.” Different sentences.

6 Prior art

Both uses have shipped elsewhere, repeatedly. Neither is invented here; the design work in this paper is fitting them into C++’s grammar.

6.1 Infix application of named callables

Haskell has spelled it this way since the first Haskell Report (1990): an ordinary identifier enclosed in grave accents is an infix operator, and x `div` y, x `mod` y, xs `elem` ys are everyday Haskell. The descendants kept it. PureScript and Idris both apply any function infix with backticks, as do the Haskell-family dialects. PureScript is the interesting case, and what it shows is descent under pressure. Its backtick operators are left-associative at the highest precedence and cannot be given a fixity at all, which is the Haskell default and not an independent answer: Haskell 2010 §4.4.2 makes any operator lacking a fixity declaration infixl 9, and the PureScript documentation says so in as many words, that identifiers in backticks all have the same associativity and precedence, like in Haskell. However, Haskell lets a fixity declaration override that default for a backticked name and PureScript does not, and the PureScript community has twice declined to add one, once as an issue against the compiler and again as a standing change proposal on its Discourse. A fixed level, kept fixed under two requests to make it adjustable, is the level this paper proposes.

Two languages adopted the construct and then removed it. That is part of the record too. Elm dropped backticks in 0.18, citing a single function, andThen, as pretty much the only one that used the feature. Unison likewise removed it. However, both are pipeline-first functional languages, where the dominant backtick use was monadic chaining, the use |> covers directly, so the feature was carrying one use that already had a spelling. The motivating set here (gcd, dot, mul_sat, approx_equal) is binary operations, not chains, and the P2011 section of this paper keeps the pipeline and the infix operator as different constructs so that neither has to absorb the other’s uses. Elm folding backtick into its pipe is evidence for that separation and not against infix.

The demand for named infix also keeps surfacing under other spellings: Kotlin’s infix fun, Scala’s bare method infix, R’s %op% operators, Miranda’s $fn (the direct ancestor of the Haskell backtick), and Fortress’s named operators. Languages keep reinventing the feature and disagree only on its spelling. Backtick is the spelling with the deepest working precedent.

6.2 Escaping keywords as identifiers

The escape is standard equipment in the languages designed since: C#’s @-verbatim identifiers (2000), F#’s double-backtick names (2005), Nim’s backtick stropping (2008), Kotlin’s backtick identifiers (2011), Swift’s `class` (2014), and Rust’s r# raw identifiers (2018). Every one of them is younger than C++’s first standard. Rust’s motivation is on the record: r# was introduced so the 2018 edition could take try, async, and await as keywords while 2015-edition code kept compiling and kept calling functions with those names. Editions plus raw identifiers are how Rust made keyword adoption routine, and its grammar is the shape of the one proposed here; §“The escape yields an ordinary identifier” makes that case.

The comparison that bears on C++ is C. Same problem, same era, a different answer: the reserved spelling. _Bool in C99, then _Alignas, _Static_assert and _Thread_local in C11, each shipped with a header macro supplying the name anyone would actually write, and each promoted to a plain keyword in C23 once the macro had carried the migration. Choose a spelling no program could have used, and let a macro hold the good name until the good name is safe. co_await is that move without the macro.

Python is nearer still, is taking keywords now, and has no escape either. It has 35 reserved words and no way to spell one as a name, so when the language wanted match it did what C++ did with module: match, case and _ are soft keywords in 3.10, reserved only inside the match statement, as type is in 3.12. Context-sensitive grammar, arrived at independently. Everywhere else the workaround is institutional, and PEP 8 writes it down: “it is generally better to append a single trailing underscore rather than use an abbreviation or spelling corruption. Thus class_ is better than clss.”

Three answers to a keyword collision are in use: break the code, mangle the keyword, or make the grammar context-sensitive. C++ has used the second and the third, and so has C. The fourth needs a spare token, which is why the languages that have one are the ones designed with a token to spend. C++ has one left.

7 Design choices and decisions

The implementation work was run against a decisions log. The decisions that shape the design are argued here, one to a section, so that EWG can poll any one of them on its own.

7.1 It desugars to the call, and that is the whole design

The single most consequential decision is that the operator lowers to a call expression in the front end, before overload resolution. It is not a macro, a token rewrite with its own rules, or a new expression category with its own type rules. The compiler builds the same call node it would have built for f(x, y).

Everything else follows. If f(x, y) compiles, x `f` y compiles, with the same result. If f(x, y) is ambiguous or ill-formed, so is the backtick form, with the same diagnostic. ADL applies because ADL applies to the call. Templates, SFINAE, and constexpr work because nothing new was added for them to fail at. The specification burden on CWG is correspondingly small: one grammar production and a definitional rewrite.

7.2 Precedence: the highest binary operator, and why not higher

Backtick binds tighter than * and looser than the unary and postfix operators. Both operands are cast-expressions, so prefix operators attach symmetrically:

-a `f` -b               // f(-a, -b)     — symmetric
a * b `f` c             // a * f(b, c)   — tighter than *

We considered the still-tighter alternative, binding above unary, so that -x `f` y would read -f(x, y). There is a real intuition behind it (“the named operator is the tightest thing there is”). However, it was rejected on its own consequences: it makes backtick the only operator in the language where a leading prefix operator floats out of its operand, so -a `f` -b would mean -f(a, -b). Asymmetric, and hard to teach. Consistency with every other binary operator won. The sole cost is that -x `f` y is f(-x, y), which is the consistent reading anyway. PureScript sits at this level too, inherited from Haskell along with the construct, and has kept it there with no way to override it; the prior-art section has the history.

There are three answers to fixity, and this proposal takes the third. Haskell declares it, one infixl per name, which makes the parse of an expression depend on which imports are visible and lets two translation units disagree about what the same tokens mean. A language of symbolic operators can instead derive it, as OCaml does from an operator’s first character. However, that answer is available only to symbols. The slot here is an expression rather than a spelling, so x `get_op(k)` y offers nothing to derive from, and a rule keyed on bare names would make x `f` y and x `(f)` y group differently. PureScript fixes it, and so does this proposal. A fixed level makes the parse of an expression depend on nothing but the expression; only the meaning of the slot travels, and that is ordinary lookup.

7.3 Left-associative

a `f` b `g` c           // g(f(a, b), c)

Chains group in reading order, like - and /. Nothing more to it.

7.4 The operator slot is an assignment-expression

Anything you could write as the callee of a call is admitted — a qualified name, a member access, a lambda — excluding only a top-level comma. The operands, being cast-expressions, exclude braced-init-lists; x `f` {1,2} is not admitted in this proposal. The brace form is meaningful as a call argument, and could be revisited, but a leading-brace left operand collides with block syntax, and the workaround is to write the call. We took the restriction.

7.5 Bare nesting is chaining; real nesting takes parentheses

The open and close delimiter are the same token, so the slot can never contain a bare backtick; the first interior backtick closes the slot. What looks like nesting is therefore token-identical to a left-associative chain, and that is how it parses.

x `f `g` h` y           // a chain:  h(f(x, g), y)
x `(f `g` h)` y         // nested:   (g(f, h))(x, y)

This cannot be diagnosed without contradicting left-associativity, and it does not need to be. It is the same regrouping-changes-the-answer situation as a - b - c versus a - (b - c), which no compiler diagnoses either: the grammar groups, parentheses override. The language defends against honest mistakes, not against a type engineered to be simultaneously callable, value-convertible, and asymmetric, deployed with the parentheses omitted on purpose.

7.6 Evaluation order is the call’s

x `f` y adds no evaluation-order rule. Operand order is unspecified, as in 7.6.1.3 Function call [expr.call]; since C++17 the callee (the slot, though written between its operands) is sequenced before both of them. Source order is not evaluation order for any other call in the language, and this is a call.

7.7 A type name in the slot constructs

A type-name is call-eligible, so x `T` y is T(x, y): functional-style construction, and CTAD applies:

a `std::pair` b         // std::pair(a, b)

The grammar says so explicitly: the backtick-operator production accepts a simple-type-specifier or typename-specifier alongside assignment-expression, and a slot that names a type takes the type interpretation. (A bare type-name is not an assignment-expression, so without those productions a `std::pair` b would be a grammar contradiction.) The result is always an expression, whichever production the slot takes, so no most-vexing-parse declaration reading can arise.

7.8 The escape yields an ordinary identifier

The keyword escape does all of its work in the parser and none anywhere else. `new` in a name position produces an ordinary identifier whose spelling is new; lookup, overload resolution, mangling, and linkage proceed as if the word had never been a keyword. There is no lexer identifier-synthesis, no new name category, no ABI surface. What the escape buys is what Swift, Kotlin, F#, and Rust bought with theirs: the committee can claim a good word as a keyword without breaking the programs that already use it, and a program that must interoperate with one of those languages, or with its own past, can name the entity it needs to name. The one consequence users see is printing: because the escape is spelling and not identity, a diagnostic and a pretty-printer put the backticks back, and both compilers do, GCC with one surface, the name of a type, still printed bare.

The word between the backticks does not have to be a keyword. Anything spelled as an identifier may stand there, and what comes out is that identifier and nothing more specific. `foobar` is foobar: the same entity, found by the same lookup, with the same linkage and the same mangling, and a program may write the name either way in either place. Every other rule about identifiers then applies to the result unchanged. A reserved name stays reserved, so `__foo` buys nothing that __foo does not already cost. An object-like macro name is still replaced, because the escape is a phase 7 construct and phase 4 has never heard of it. A function-like macro name is not, for the preprocessor’s own reason: it is replaced only when the next token is (, and here the next token is the closing backtick. Both forks behave this way. The backticks neither shield a name from the preprocessor nor expose one to it.

An earlier draft of this paper restricted the content to words that actually are keywords, so that `foo` was ill-formed rather than a noisy spelling of foo, and left the choice to EWG. The restriction is withdrawn, for the reason the motivating example gives: an escape that only accepts words that are already keywords cannot be written until the standard that takes the word has shipped, so it can repair a break and can never prevent one. Two more consequences of the restricted rule are worth the room’s attention. A tool that generates C++ would have to carry the keyword list, per dialect, and would be wrong on the day the list changes, which is the day it was supposed to help. And the backticks would acquire a meaning of their own: a reader would have to know whether the word is a keyword in this dialect before knowing whether the line is well-formed. Under the rule proposed here the backticks say only “this is a name”, which is true whatever the word is, and the question never has to be asked.

The alternative representations, and, bitor and their nine siblings, are escapable under the same rule and are meant to be. In C++ they are tokens and not macros: identifier-shaped words the language has claimed, which is the category the escape exists to release, and suppressing that is what an escape is for. Nothing makes them exceptional, so nothing should except them, and excluding them would put a list back where the value of the rule is that there is no list. They also show the printing rule doing its job unaided: an entity named and prints as `and`, because a bare and lexes as && and would not re-parse, while `foobar` prints bare because foobar does. One predicate answers both.

Rust’s raw identifiers, cited below as prior art for the hatch, are prior art for this rule as well: the production is r# followed by an identifier or a keyword rather than by a keyword list, and the reference is explicit that the prefix “is not included as part of the actual identifier” (the Rust reference). That is both halves of what is proposed here, in the language that installed its hatch most recently and for this reason.

EWG can take the restricted rule instead. It is one clause in a parser predicate, and in the wording below one sentence of [lex.name]. What it costs is that the escape becomes unwritable in the dialect where it does the most good, the one before the keyword lands. Both forks implement the unrestricted rule, and the implementation section below says what the change cost.

One consequence is user-visible, and it is settled here. The escape is part of the name’s spelling and not of its identity, so a printer holding the compilation’s language options puts the backticks back: a pretty-printed declaration comes out as void `new`();, since void new(); is not a program and a printer that emitted it would have lost the source, and a diagnostic names the entity `new` for the same reason, that text copied out of a diagnostic should be text the reader can paste back. The AST dump keeps the bare word where it names the declaration, which is the evidence for the paragraph above: the name really is an ordinary identifier, and the backticks are how it is written. The two implementations are not of one mind about where that line falls, and they diverge from opposite ends: Clang dumps a declaration’s name bare and the same name inside a type escaped, while GCC escapes the name of a declaration and prints the name of a type bare. Neither split is visible to a program and neither touches acceptance. Both implementations put the escape back where it matters, and it cost them the same thing. The escape yields the ordinary interned identifier and keeps no record of how it was written, so neither compiler can ask a name whether it was escaped; each has to decide instead which printing surfaces name an entity, and put the backticks back only there. Clang draws that line by the kind of argument a diagnostic was given, across six sites. GCC draws it in the one routine that prints the name of a declaration, plus a guard on the parser’s own error printer, which hands a raw keyword token to that routine as though it were a name. Both got the line wrong once before getting it right, and the symptom was the same both times: a program containing no backtick at all had its diagnostics change under the flag.

The half GCC gets wrong is the clearest evidence for what the paragraph above claims the cost is. GCC’s routine is the name of a declaration. A class or enum type is printed somewhere else, so a program that declares struct `union` { }; and then misuses it is told that ‘struct union’ has no member named ‘new: one sentence, two names, one of them escaped and the other not, because the two halves arrive from two printers. Clang escapes both. No program is accepted or rejected differently; what fails is the thing the decision exists to deliver, which is that text copied out of a diagnostic can be pasted back. Deciding which surfaces name an entity is the whole cost of the feature’s printing, and a compiler can pay it in one place and not another without anything failing.

The cost is bounded added context-sensitivity: tentative declaration-versus-expression parsing must recognize escapes, and tooling must distinguish the two uses. Both implementations do.

7.9 One spelling

Backtick is the sole proposed spelling; there is no digraph and no alternative token. The objections to backtick (it is Markdown’s inline-code delimiter, and a dead key on some keyboard layouts) are real and minor, and neither is a capability gap: CommonMark’s multi-backtick spans already express x `f` y in running prose, and fenced blocks, the dominant case, are unaffected. That workaround renders correctly today on GitHub, on the committee’s own Mattermost server, and throughout the source of this very paper, which is written in Markdown and is by now littered with inline renderings of the single-backtick form. Against that, a second spelling doubles the teaching, formatting, pretty-printing, and tooling surface permanently, and fragments the one-recognizable-form spelling the readability argument rests on. Trigraphs were removed in C++17; digraphs are vestigial. We decline to mint a new one.

The analysis of candidate alternative spellings is carried in an appendix, with the rebuttals stated, so the question can be settled against the record in this paper. It includes the one pair, \< … \>, that would actually be better-engineered, and why adopting it would be choosing a different operator rather than aliasing this one.

7.10 Two uses, one paper; no library

The infix operator and the keyword escape share one lexical token and one committee, so they are proposed jointly: one “what does backtick mean” discussion in EWG, not two, and no chance of two independent papers designing the token into contradiction. The library layer is the opposite case: pipeline and composition helpers (pipe, mbind, and friends) are each a few lines of user code, would route the paper through LEWG as well, and the operator needs none of them to function. The rule is: bundle what shares a design surface within one committee; split what is separable across committees. So the two language uses travel together, and any standard helpers wait for a companion library paper once usage shows which, if any, deserve it.

8 Anticipated objections

8.1 “Define * on a type instead”

The objection: named binary operations do not need an infix spelling, because C++ already has one, which is to overload an operator on a type. Saturating multiplication does not need a `mul_sat` b; it needs a Saturating<double> whose operator* saturates.

Note first that the lift is not optional. Overloaded operators require a class or enumeration operand, so double * double cannot be given new meaning at all; to change what * does to two doubles, inventing a type is the only move the language offers. The objection is not “there is a lighter alternative”; it is “the heavyweight alternative already exists.” And the committee has already decided this exact example, in the library: C++26’s saturation arithmetic ([P0543R3]) is std::add_sat, std::sub_sat, std::mul_sat, std::div_sat, named free functions in <numeric>. No saturating wrapper type was shipped. As with std::gcd, std::midpoint and std::lerp before it, the library keeps choosing names, because the type encodes the wrong thing.

Types are not free in C++. Haskell writes newtype Sat = Sat Double — one line, guaranteed zero representation cost — and even there the wrapping and unwrapping is felt as ceremony. C++ has no newtype. A usable Saturating<T> is a constructor set; a conversion policy, where explicit is safe and noisy and implicit is quiet and dangerous; the rest of the operator zoo, forwarded; and an interoperation debt everywhere the wrapper meets existing code: is_arithmetic says no, numeric_limits wants a specialization, and every function that takes a double now takes a .value(). That is a real class to design, review, document, and maintain, as a workaround for one function lacking an infix spelling.

And the type is the wrong scope. Wrapping a value makes every operation saturating for as long as the wrapper is on, when the intent was one multiplication in one expression. Saturating versus wrapping versus trapping is a property of an operation and not of an object. The wrapper cannot compose for the same reason: operator* can mean only one thing per type, so an expression that needs a saturating multiply and a wrapping add has nowhere to stand. a `mul_sat` b `add_wrap` c says it directly, at the site where each choice applies.

Last, the lift is noise in the place the objection claims to remove it. Saturating{a} * b reads worse than a `std::mul_sat` b, and it misdirects: it marks the data as special when the operation is. The reader must go find out what Saturating does to *; the named function said it in the expression.

The wrapper type is what we write today because the call syntax reads worse than the operator syntax. This proposal fixes the syntax instead.

9 Fitting the grammar, without collision

9.1 The token is free

Backtick has no meaning in C++ source today outside string literals, character literals, and raw-string delimiters, all of which are handled in translation phase 3 before punctuator recognition, and are therefore unaffected. A stray backtick in program text is ill-formed in every current compiler (stray '`' in program, in GCC’s words). Only three printable ASCII characters are unclaimed at all: the backtick, the dollar sign, and the commercial at. And the other two are compromised: the dollar is an identifier character under the default-on -fdollars-in-identifiers in both GCC and Clang, and @ is the Objective-C sigil in a lexer Clang shares between the languages. Backtick is the entire remaining inventory.

9.2 The productions

user-infix-expression:
    cast-expression
    user-infix-expression ` backtick-operator ` cast-expression

backtick-operator:
    assignment-expression
    simple-type-specifier
    typename-specifier

The second and third alternatives are the type slot, and the type reading wins exactly when lookup finds a type or a class template. The rule does not follow from the expression slot: a bare type-name is not an assignment-expression, so a grammar with only the first alternative would contradict the section above rather than imply it.

user-infix-expression slots between cast-expression and pm-expression: the pointer-to-member productions consume a user-infix-expression where they consumed a cast-expression, and everything above them is unchanged. In implementation terms this is one new top level in each compiler’s binary operator precedence table. The left recursion gives left associativity; the cast-expression operands give the symmetric prefix binding argued for above.

The new level is not a fold-operator: (... `f` N) is ill-formed. Both implementations reject it, each with an ordinary parse error that says nothing about why, which is the reason for stating the exclusion here: nothing else would say it was chosen. Excluding costs one clause in the predicate that already decides which operators may be folded over; admitting would require a fold-expression node that can hold an arbitrary slot expression, which today’s cannot. Nothing is foreclosed; every program a later revision would newly accept is one this proposal rejects.

The keyword escape is a new identifier alternative in name positions:

escaped-identifier:
    ` identifier `

yielding an identifier token whose spelling is the word between the backticks. The identifier in that production is the lexical one, the production in [lex.name], which every keyword matches; [lex.key] is what makes a keyword out of one of those matches, and inside an escape it does not apply. So the escape admits any word spelled as an identifier, and what it yields is an ordinary identifier. It may appear wherever the grammar uses identifier as a terminal, and nowhere else: a declarator-id, a class-head-name, an enum-name, an enumerator, a namespace-name, a template parameter’s name, a mem-initializer, a label, a primary-expression, an id-expression after ., -> or ::.

9.3 The same-delimiter problem

The open and close delimiter are the same token, so while parsing the operator slot, a naive expression parser would take the closing backtick as the start of a second, nested backtick operator. C++ has, of course, been here before: > inside a template-argument list is a closer, not an operator, and vector<vector<int>> is handled by a parser flag — Clang’s GreaterThanIsOperator, GCC’s greater_than_is_operator_p — that turns the operator meaning off in that context. We do the same thing: a BacktickIsOperator flag, false while parsing the slot, restored inside any nested parentheses or brackets so that parenthesized nesting works. Both implementations are modeled line-for-line on their compiler’s existing > handling. This is the one genuinely novel parsing obligation the operator carries, and it is a solved problem with fifteen years of production precedent.

9.4 Operator and escape never meet

C++ expression grammar strictly alternates between wanting an operand and wanting an operator. The escape lives exclusively in operand positions; the infix operator lives exclusively in the post-operand position. The positions are mutually exclusive, so one lexical token serves both uses with no lookahead and no ambiguity, the same strategy the language already uses to give *, &, and < their multiple readings.

int `new`(int, int);  // declarator-id  -> escaped identifier "new"
`new`(a, b);         // primary        -> call to the function named "new"
obj.`delete`();      // after '.'      -> member named "delete"
x `f` y;             // post-operand   -> infix: f(x, y)
x `(`new`)` y;       // escaped callee -> (`new`)(x, y), parenthesized slot

An earlier draft claimed a second and independent signal here: that an escape wraps a keyword, which is never a valid callee expression, and a slot wraps an expression, which is never a bare keyword. Since the escape wraps any identifier, that signal is gone. The design is unaffected, because it never rested on the signal, and the case where the two token sequences now coincide shows why. Read as an escape and read as a slot, `f` names the same thing, f; the two readings differ only over whether f is an operand or a callee, and that is precisely what the position states. Position alone suffices, as it does for *, &, and <.

Because the escape yields an ordinary identifier, nothing downstream of the parser changes: no new lookup rules, no mangling scheme, no ABI surface. void `new`(); links as a function named new.

10 Implementation experience

Both features are implemented, gated behind an opt-in -fbacktick flag, in two independent compilers, with tests, and each compiler’s full regression gate stays green with the flag off and on. This section is what building them changed in the design and what building them got wrong. Both are summarized here and expanded in the subsections that follow; a reader who writes compilers for a living can take the summary and skip the rest.

What the implementation changed. Four things in this paper are there because the compilers demanded them.

What went wrong. Six defects, all found after the prototypes were finished, none of them a design question, and all fixed except where the next paragraph marks them open:

  1. ADL. Clang’s slot had no ADL at all; GCC’s had it for a bare name but not for a template-id. The shape that catches this, a visible and viable candidate beaten by one reachable only through ADL, was in neither test suite.
  2. Escape coverage. Four sweeps found four sets of gaps: declaration positions, use positions, qualified names, and keywords that are also type names (GCC rejected int `int` = 0; for two months).
  3. Printing. Each new escape position printed the bare keyword until it was routed through the routine that restores the backticks. The operator printer lacked two arms: aggregate initialization, which failed silently by printing a different program, and class-typed callables, every lambda in the motivation section, which failed loudly with an inverted source range.
  4. A parser loop. Clang’s implicit-int recovery re-entered on an unresolvable qualified escape and consumed nothing. A coverage sweep can not find that; running the error cases under a timeout did.
  5. The flag leaked. Clang’s flag reached C compilations; GCC’s parser tested the flag without testing the token. One line each, found by comparing flag-on against flag-off output on a program that never mentions the feature.
  6. The transparent node. Clang’s source-fidelity wrapper had to be taught to the static analyzer and its bug reporter, the code generator, libclang, and the AST matchers, and every one of those sites is quiet when it is wrong. GCC desugars in the parser and pays none of it.

Still open. Two sets of programs are treated differently by the two compilers under the flag. GCC does not implement the type-name slot, so 1 `Pt` 2 is rejected there and accepted by Clang. And inside a template, GCC keeps only the ADL half of an unqualified slot’s lookup at instantiation, discarding the definition-context lookup that 13.8.4.2 Candidate functions [temp.dep.candidate] requires, so t `pipe` inc is rejected where pipe(t, inc) compiles in the same translation unit. Clang has it right; the GCC prototype has a bug to fix. Nothing the keyword escape does is on either list. One GCC diagnostic does still print an escaped declaration name beside a bare type name, which is a printing gap and not an acceptance one.

10.1 The forks

Both forks are public:

The GCC branch was later rebased over three months of upstream trunk: 2158 commits, 177 of them touching the C++ front end, the front-end infrastructure it shares with C, or the preprocessor. Every line the feature adds or removes came across unchanged, with no conflict. Nothing the feature touches had moved under it. For a design whose whole claim is desugar and inherit, that is the number that matters: a diff of this shape has very little to catch on.

10.2 Escape coverage: four sweeps

The escape works in both compilers in every name position the wording admits, and the wording’s position list is the product of that work rather than its source. The positions that declare a name: declarator-ids (variables, functions, class members, typedef names, parameters, a friend declaration’s name, the qualified name in an out-of-class member definition), a class-head-name, an enum-name scoped or unscoped, an enumerator, a namespace-name, a type, non-type or template template parameter’s name, an alias-declaration’s name, an alias template’s, a concept’s, a mem-initializer, a label, and expression positions including after .. Declaring a name is only half of a hatch, so the positions that use one are prototyped too: a type-specifier, a base-specifier, a nested-name-specifier, a template-name being specialized, a using-directive, a type-constraint, and a constructor’s name. And a qualified name may be escaped at either end or at both: N::`union` g;, using X = N::`union`;, sizeof(N::`union`), typename T::`union`, `module`::inner::f(). The rest of this subsection is how that list was reached: four sweeps, what fixing what they found cost, the parser loop the fix shipped, and the three cross-compiler divergences met on the way, each of which closed as a gap and not a disagreement.

That coverage is recent, and how it was arrived at is a fair warning about what “implemented” means for a grammar extension. Both prototypes were finished, and both were then found to take the escape in whichever positions their parser happened to route through the routine the escape had been written into, and to refuse it wherever a bare identifier token was read somewhere else. That is why a concept’s name worked and struct `union` { }; (the example in the wording below) did not, in either compiler. Nobody had drawn the boundary; it fell out of two independent parsers, differently in each. Neither test suite contained a negative test for any of it, so nothing was failing and nothing would have failed.

It was found by writing one program per position and compiling them. That has now been done four times, and it has found something on all four. The first sweep covered the positions that declare a name; the second, written after somebody noticed that a type nothing can name is no use, covered the positions that use one. The third covered qualified names, and found that Clang read the final component of a qualified name as an unqualified-id only when it named an object or a function, so N::`new` had worked from the first day and N::`union` had never worked at all. The fourth changed the keyword. Every program anyone had written used new, class, union or try, which are pure keywords; int is not, and GCC rejected int `int` = 0; in the first and best-tested position in the table, and had done for two months. Seventy-nine programs now, in four groups, and the whole sweep runs in about two seconds. It is checked into the repository, which it should have been three sweeps ago.

There is a fifth thing those seventy-nine programs never vary, and it is not a position. Every one of them escapes a keyword, because until this revision the rule required one. The fifth sweep changes the word: nineteen programs that escape an ordinary identifier, an alternative token, and a word that is a keyword only in a later standard, in the positions the first four covered. Before the change both compilers rejected all of them. After it, both accept all nineteen, under -std=c++17 and under -std=c++20 alike, and bool `requires`(int);, which each compiler had taken only in the dialect where requires is a keyword, is now the same declaration in both.

The change was a single predicate on each side: Clang asked IdentifierInfo::isKeyword in the two routines that recognise and consume an escape, and GCC asked for a keyword token in its escape arm and in the lookahead that steps over one. Nothing else in either parser moved, and the reason is the identity rule itself. Neither compiler records that a name was escaped, so neither can print one differently, and a name whose spelling is not a keyword prints bare, which is what the rule says it should do. One printer did need a line. GCC decided whether to escape a name in a diagnostic by asking whether it was a keyword, and an alternative token is not one there: cpplib turns and into && before the parser sees it. So GCC named a variable declared as `and` with a bare and, which does not re-parse. It now asks the preprocessor’s question as well, and both compilers print `and` escaped and `foobar` bare.

What it cost to fix is small, though not the number first estimated. The escape parse becomes a helper called from each name position — twenty call sites in Clang, one arm plus its guards in GCC — and then three things nobody had priced. A parser that decides what it is looking at from the token after a name has to step over three tokens where it stepped over one, so every such lookahead is a call site too; a label is told from an expression statement only by the : that follows it. A new name position is a new printing surface: enumeration names, namespace names, template parameter names, labels and nested-name-specifiers all printed the bare keyword, which is source that does not re-parse, until they were routed through the one routine that puts the backticks back. And a parser that caches tokens for backtracking has a third cost the other two do not imply. Clang collapses a resolved qualified type name into a single annotation token and matches that token against the cached stream by source location; a name written as an escape occupies three tokens, so the annotation has to begin on the opening backtick and end on the closing one. Get either end wrong and the cache is left holding a stray ` in front of the annotation, which the next backtracking parse resumes on. GCC pays none of that, because it does not cache and re-annotate. More than half the work was in those three, and none of them appears in the grammar.

The loop came from that last change. Clang’s recovery for a qualified name it cannot resolve is to try implicit int; that does not apply to an escape and consumes nothing, so namespace N { int x; } N::`union` g; re-entered the same case with the same tokens indefinitely. The code it replaced had been avoiding that by accident, by giving up as soon as it saw a backtick. Neither test suite covered a malformed or unresolvable escape in a qualified position (neither covered a qualified escape at all), and a diagnostic-matching test would not have caught it in any case, since a test that never terminates does not fail. What caught it was running the error cases under a timeout, which is a different question from the one a coverage sweep asks and needs its own harness.

Three cross-compiler divergences were found on the way, and every one of them left the same way: it turned out to be a gap rather than a disagreement, with a single cause behind however many programs it showed up in. The last two are the instructive pair, because they ran in opposite directions. GCC rejected an escape whose keyword is a type keyword, because int and char and their siblings are bound at global scope to the builtin type in GCC’s name table, so the name the escape yields was already taken. That looks like a representation the design would have to pick a side on. However, in C++ a declaration can be named by a keyword only if it was escaped, since int is a keyword token everywhere else and the declarator check rejects a bare reserved word; a collision with that binding is therefore never a redeclaration, and the fix is to say so, at the three places GCC consults it. int `int` = 0; compiles, int still names the builtin in the same translation unit, and g(int, `int`) mangles as _Z1gi3int in both compilers. Which is the ABI claim above, demonstrated on the hardest name the feature has.

The other of that pair ran the opposite way, with GCC briefly the wider implementation: it took N::`union` where Clang did not. One arm in the routine that reads an identifier reaches every name position GCC has, a qualified type among them. Clang reads a qualified type name somewhere else entirely, and in three somewhere-elses: the declaration-specifier path, the typename-specifier path, and the tentative parse that decides whether a statement is a declaration at all. Twelve programs, four arms, and then a fifth to put back a constructor definition the first four had broken: `union`::`union`() { } had been working by accident, on the strength of the old code giving up early. No design question anywhere in it.

10.3 The type-name slot, in one compiler

The type-name slot has single-compiler evidence. Clang implements it: a bare name looked up as a type with a deduction placeholder, a qualified one through a tentative parse, a builtin through the functional-cast path, all three routed to the T(x, y) build, which is where CTAD and temporaries come back for free. However, GCC parses its slot as an expression, so 1 `Pt` 2 is rejected there.

10.4 Printing: which surfaces name an entity

Both compilers restore the backticks wherever an entity is named, in a pretty-printed declaration and in a diagnostic, and keep the bare word in the AST dump; the design section argues why. The cost of that decision is deciding which printing surfaces name an entity, and both compilers paid it twice, once for the escape and once for the operator. Clang’s -ast-print now round-trips every shape the operator can take, with one exception a reviewer will find: a slot naming a builtin whose call the semantic layer rewrites into a node that is no longer a call (a `__builtin_shufflevector` b) prints as the rewrite, because the rewrite is not expressible in the syntax at all.

The escape’s printing cost is in the sweep account above: every new name position was a new printing surface, and each printed the bare keyword, which is source that does not re-parse, until it was routed through the one routine that puts the backticks back. The one gap left is GCC’s, described in the design section: it escapes the name of a declaration and prints the name of a type bare, so a single diagnostic can carry both spellings.

The operator’s printer failed twice more, and both were found the same way, by re-deriving this paper’s claims against the compilers rather than reading them off the implementation. How they were missed is the general point. The printer and the source range both recover the operands from whatever the semantic layer built, so every node that layer can hand back needs its own arm. A type slot naming an aggregate does not construct through a constructor; it initializes through parenthesized aggregate initialization and comes back as a different node. A slot whose value is a class-typed callable (every lambda, every function object, every helper the motivation section is written on) is called through the object’s own operator(), which the semantic layer keys as an operator call: the slot lands at argument zero and the operands shift one place along. Both arms were missing.

The two failed in opposite ways. The aggregate arm failed silently. It printed the desugaring, which was well-formed, plausible, and not what was written; in the deduced case it printed a cast applied to a comma expression, a different program altogether. The callable arm failed loudly. It printed text naming operator() as a free function, which does not compile, dropped an operand, and reported a source range whose end preceded its beginning. Neither was caught, because the round-trip test had no case of either shape, and a claim tested only where the printer already works is untested whichever way it fails.

A round-trip claim is a claim about every node the semantic layer can build, not about the nodes the printer was written against. That sentence was written for the first of the two, before anyone knew there was a second. The second was found afterwards, by asking the question the sentence asks, which makes it the better evidence: a general statement that catches a further instance of itself, in the same paper, after it was written down. Both arms are written now, and the exception above is all that is left.

10.5 Argument-dependent lookup, which both implementations got wrong

ADL fidelity is normative in this design: x `f` y must not have quietly weaker lookup than f(x, y). Neither compiler delivered it on its first attempt, and the two failures were the same failure: the name in the slot was resolved before the call builder ever saw it. Both deliver it now outside a template. Inside one, GCC still does not: it discards the definition-context half of a dependent slot’s lookup at instantiation, which is the open case in the summary above, and the last paragraph of this section describes it.

GCC took two goes. Its first cut resolved a bare-name slot at parse time, so a call depending on pure ADL — the callee visible in no enclosing scope, only in an argument’s namespace — failed there and compiled elsewhere. The fix routed the slot through the same Koenig lookup a plain call performs, but recognized only a bare name, so a slot carrying template arguments kept the old behavior silently for another round.

Clang started further back: its slot was parsed with the ordinary expression parser, which resolves the name before the call builder is reached, so the slot had no ADL at all. A hidden friend in the slot was use of undeclared identifier. And in the shape that matters, nothing was said at all: with an ordinary-lookup candidate visible and viable, and a better candidate reachable by ADL, u `pick` u bound the visible one while pick(u, u) bound the ADL one, no diagnostic anywhere. The operator called a different function from the call it is defined to be.

That defect produced the strongest evidence in this paper for the desugaring thesis, and it is inside one compiler rather than between two. The Clang build carrying the backtick operator also carried a second infix experiment (user-defined operators spelled with Unicode symbols, a companion design not proposed here) whose slot never becomes an expression: Sema performs its own operator lookup and hands an unresolved set to candidate assembly. One build, one machine, one author, one difference. The feature that reached the call builder unresolved inherited ADL from its first commit, without anyone deciding to inherit it; the feature that resolved its slot first had to be repaired. That is the whole thesis, with the compiler and the author held constant.

The near-miss tells a reviewer as much as the fix does. The defect survived the entire implementation because the one test that announced itself as the ADL case used a qualified name in the slot, which correctly gets no ADL either way; it passed whatever the slot did, and its heading was enough to stop anyone writing the test that would have failed. The shape that catches this is augmentation rather than “does it compile”: an ordinary-lookup candidate that is visible and viable, a better ADL candidate, and the choice made observable in the result type. That is the only shape in which weaker lookup on the slot produces no diagnostic at all, and no implementation should be believed without it.

The open case is this section’s own rule failing on the other side. Inside a template, an unqualified slot naming something argument-dependent lookup cannot reach, a function brought in by a using-declaration for instance, is rejected by GCC and accepted by Clang. GCC re-runs the lookup at instantiation and keeps only the ADL result, discarding the ordinary lookup from the definition context that 13.8.4.2 Candidate functions [temp.dep.candidate] requires it to keep. A variable fails the same way, and an ADL-reachable name is accepted, so what is lost is ordinary lookup itself rather than some narrower rule about what ADL may find. What makes the reading unambiguous is that the plain call still compiles: in one translation unit, pipe(t, inc) is accepted where t `pipe` inc is not. That is the slot carrying weaker lookup than the call it desugars to, which is what this section opened by ruling out. Clang is the conforming implementation and the GCC prototype has a defect to fix.

10.6 What the AST node costs, and which compiler pays it

Clang builds a source-fidelity node, a transparent wrapper around the desugared call, and that node is why -ast-print reproduces the written syntax. GCC desugars in the parser and hands its semantic layer an ordinary call. The two accept the same programs and generate the same code, so they differ in kind and behave identically. The node has a price, and a reviewer should attribute the price correctly: it is not the node, it is the transparency. A wrapper the rest of the compiler is meant not to notice is a wrapper nothing will remind you to teach anything about, and the sites that need teaching are quiet when they are wrong: six in the static analyzer’s modelling layers, a seventh in the bug reporter created by meeting the other six, four arms in the code generator, the exhaustive statement-class switches, the libclang cursor map, the AST matchers. Exactly one of the analyzer’s seven announces itself, and only as a warning in a build log.

The seventh is the instructive one. Teaching the control-flow graph to look through the wrapper leaves the wrapper with no program point of its own, so the bug reporter’s tracking chain is abandoned before a single handler runs, and with it the suppression, on by default, that keeps core.NullDereference quiet about a null returned from an inlined callee. p `identity` 0 reported a false positive that the identically-desugaring identity(p, 0) was spared, and the report it did emit carried two path notes where the call’s carried eight. The operator form was noisier than the call it is sugar for, and explained less. One arm in the reporter’s peeling routine fixes both symptoms at once, because peeled early the two forms are one expression for everything downstream; the two reports now agree note for note.

That is the cost of source fidelity, and round-tripping the written syntax is the payoff. A front end that desugars in the parser pays none of it, and gets none of it.

10.7 The gate fails quietly

A prototype behind a flag has one obligation ahead of the feature itself: with the flag off, nothing changes. Both implementations broke it in the same shape, and neither break showed up in a diagnostic.

Clang’s -fbacktick reached the language options in a C compilation, where the C++ grammar it enables has no business being, so int f(int a, int b){ return a `g` b; } compiled as C. Nothing lost a diagnostic; an invalid program was accepted. GCC’s two backtick cases in the parser were fall-through targets for other tokens and tested only the flag, not the token, so with the flag on, a program containing no backtick could take a different path and be diagnosed differently.

Both were one line. Both were found by comparing flag-on output against flag-off output, and that is the check to ask an implementation for: the two compilations must produce byte-identical output on a program that never mentions the feature. Whether the flag produces the right diagnostic is a different question, and a weaker one.

10.8 What the parsers confirmed

Both implementations, built separately from the same design, accept the bare “nested” form as a left-associative chain, because the token stream for the two readings is identical. What the design predicted on paper, two unrelated parser architectures reproduced. Nesting-is-chaining is a consequence of the grammar. Clang carried a diagnostic for the bare form through most of the implementation and it never once fired; it was deleted rather than made to fire, since making it fire needs the lookahead that would have to reject legal chaining too. A diagnostic that cannot fire is a claim the grammar has already withdrawn.

The motivation section is implementation experience as well. Every pattern in it (pipe threading, the range-adaptor closures, the bind_back stages, then composition, the short-circuiting implies and the mbind chain) was compiled and run against the built -fbacktick Clang, C++23, -Wall -Wextra clean. The ranges comparison was verified to produce identical results and identical laziness through | and through `pipe`, on the same closure objects.

11 The design, in one place

Everything argued above reduces to the following, and the wording that follows says no more than this. Every item is decided, and every item is implemented in both compilers except where noted.

The infix operator.

The keyword escape.

Shared.

Left to EWG. One question, which this paper answers rather than leaves open: whether the escape is restricted to words that are keywords, so that `foo` is ill-formed rather than a second spelling of foo. The proposal is the unrestricted rule, for the reason §“The escape yields an ordinary identifier” gives — a restricted escape cannot be written until the standard that takes the word has shipped, so it can repair a break and can never prevent one. Both implementations support either answer, so EWG can still poll it.

12 Wording

The following wording is pro forma. It is intended to make the grammar change concrete; it is not offered as final CWG wording. Wording is relative to the current working draft.

12.1 [lex.operators]

Modify the operator-or-punctuator grammar in 5.8 Operators and punctuators [lex.operators] paragraph 1 by adding ` :

operator-or-punctuator: one of
       ...
       ?     ::    .     .*    ->    ->*   ^^    ~     `
       ...

[ Note: The row shown is [lex.operators]’s third; only the trailing ` is added.end note ]

12.2 [lex.name]

Add to 5.11 Identifiers [lex.name], after the paragraphs defining identifier:

escaped-identifier:
    ` identifier `

x An escaped-identifier may appear wherever the grammar uses identifier. The token between the ` tokens is treated as an identifier even if it would otherwise be a keyword or alternative token. The ` tokens are not part of the identifier. An escaped-identifier and an unescaped identifier with the same spelling are the same identifier.

x+1 [Example:

void `new`();          // declares a function named new
`new`();               // and calls it
int `count` = 0;
++count;               // names the same object

— end example]

12.3 [expr.mptr.oper]

Modify the grammar of 7.6.4 Pointer-to-member operators [expr.mptr.oper] paragraph 1:

pm-expression:
    cast-expression user-infix-expression
    pm-expression .* cast-expression user-infix-expression
    pm-expression ->* cast-expression user-infix-expression

12.4 [expr.backtick] (new subclause)

Insert a new subclause between 7.6.3 Explicit type conversion (cast notation) [expr.cast] and 7.6.4 Pointer-to-member operators [expr.mptr.oper]:

Backtick operator [expr.backtick]

user-infix-expression:
    cast-expression
    user-infix-expression ` backtick-operator ` cast-expression

backtick-operator:
    assignment-expression
    simple-type-specifier
    typename-specifier

1 An expression of the form E1 `O` E2 is identical (by definition) to O(E1, E2) (7.6.1.3 Function call [expr.call]).

2 If the backtick-operator is a simple-type-specifier or typename-specifier denoting a type T or a placeholder for a deduced class type, an expression of the form E1 `T` E2 is identical (by definition) to T(E1, E2) (7.6.1.4 Explicit type conversion (functional notation) [expr.type.conv]). A backtick-operator whose tokens can be interpreted both as an assignment-expression and as a simple-type-specifier or typename-specifier is interpreted as a simple-type-specifier or typename-specifier.

3 When parsing a backtick-operator, the first non-nested `1 is taken as the ending delimiter, rather than as the first delimiter of a nested user-infix-expression.

Annex A (Annex A Grammar summary [gram]) is updated mechanically to match.

13 Appendix A: alternative spellings, and the lexical inventory

Backtick is the sole proposed spelling. This appendix carries the analysis behind that decision: why alternatives get raised, the lexical filter any candidate must pass, the candidates themselves, and the wider inventory of what ASCII actually remains, so that if the spelling question is raised it can be settled against this record in this paper instead of reopened in a future one.

13.1 A.1 Why alternatives get raised

Two reasons, both real, both minor. A single backtick is Markdown’s inline-code delimiter, so x `f` y in running prose fights the markup. And backtick is a dead key or awkward on some non-US keyboard layouts; it was one of the ISO-646-variant characters, alongside # [ ] { } | ~ ^ \.

The Markdown objection is on the record in WG21, made against this exact character by seven authors. P3381R0 [P3381R0] evaluated twelve single-character spellings for the reflection operator, and four multi-character ones, the backtick among them, and rejected it:

The backtick has the advantage that it’s pretty small, even smaller than ^. But it has the disadvantage that backtick is used by Markdown everywhere inline code blocks, and not all Markdown implementations properly give you mechanisms to escape it. While not necessarily a show-stopper, we also just don’t think it’s good enough to reasonably pursue.

The objection is granted, and the reasoning is right for the operator it was reasoning about. However, that operator is not this one. A reflection operator is written once per reflection and sits inside dense expression text, where a backtick operator is written where the name of an operation would go; the CommonMark cost falls on different text, at a different density, and it is paid by the same span form either way.

Neither is a capability gap. CommonMark’s multi-backtick spans already delimit code containing backticks (writing `` x `f` y `` renders as x `f` y), and fenced blocks, the dominant case for code, are unaffected entirely. The span form renders correctly on GitHub and on Mattermost, the committee’s own chat server (author-verified). In the very forum where the operator would most often be typed in running text, the friction is a solved problem. What an alternative spelling buys is ergonomics for the minority case, inline prose. Nothing more.

There is also a self-test on the record: the source of this paper is pandoc Markdown, and every x `f` y in its running prose is a multi-backtick span in the source. The friction is real; each inline example costs the doubled delimiters and a padding space. However, it is demonstrably survivable: a Markdown paper about the backtick operator is the worst case the objection can construct, and the one you are reading renders.

13.2 A.2 The filter a candidate must pass

An alternative spelling is an additional token lexed by maximal munch, like the existing digraphs. To be viable, the sequence must never appear adjacent in a valid current program. Three traps, each of which has bitten a real token before:

  1. Maximal-munch theft. After a binary operator or <, a unary-capable character is already valid: a < -b, a * *p, !!x. Minting the two-character form silently changes meaning.
  2. The :: neighborhood. a<:b needed the <:: carve-out in 5.5 Preprocessing tokens [lex.pptoken] because vector<::std::string> broke. Any new <-prefixed token lives next to that scar.
  3. Universal-character-name munch. \uXXXX can begin an identifier, so a candidate whose second character is \ can split a UCN, a non-obvious break.

13.3 A.3 The candidates

Spelling
Lexically clean?
Verdict
\< … \> yes — \ is no token today, and \< cannot start a UCN front-runner, if ever forced (A.4)
<\| … \|> yes blocked socially: \|> is P2011’s operator, and it reads as “pipe”
<\ … \> no — UCN munch (trap 3) inferior twin of \< … \>; reject
(\| … \|) yes heavy; Haskell “banana bracket” connotation; reads worse than backtick
x \op\ y yes visually too light; symmetric, so it keeps the same-delimiter rule
$$ no — $ is an identifier character under default-on -fdollars-in-identifiers reject
@@ clean in C++ the Objective-C sigil, in a lexer Clang shares; reject
<: :> <% %> already digraphs for brackets and braces

13.4 A.4 The front-runner is not an alias

One candidate deserves honesty: \< … \> is lexically bulletproof and asymmetric. Distinct open and close tokens would eliminate the same-delimiter problem outright (no BacktickIsOperator flag) and with it the nesting rule, since x \<f \<g\> h\> y parses unambiguously with no parentheses. That is, on engineering grounds, a better-designed operator than the backtick.

However, it is important to state plainly what adopting it would mean. It would be choosing a different primary spelling, a different operator with a different feel and none of the Haskell lineage. It would not be an alias. The decision taken here is backtick as the single spelling. We considered the better-engineered stranger and chose the familiar borrowed spelling, on purpose.

13.5 A.5 The rebuttals, pre-loaded

  1. It is ergonomics and not capability. Inline prose works today via CommonMark spans (A.1); fenced blocks cover code.
  2. Two spellings is a permanent tax. Teaching doubles; clang-format must pick a canonical form and normalize to it; -ast-print must choose; grep, linters, and tooling grow a second case, forever, for a cosmetic win.
  3. Direction of travel. Trigraphs were removed in C++17; digraphs are vestigial and periodically floated for removal. A new alternative token invites “and will you deprecate this one too?”
  4. It fragments the spelling. The readability case rests on one recognizable form; two camps undercut it.
  5. None reads better. Backtick is the established infix-quote idiom. The alternatives carry foreign connotations: pipe, escape, banana.
  6. If the markup clash warranted a change, it would argue against the primary and not for a second spelling. We weighed that and chose backtick-primary anyway. An alias is the worst of both worlds.
  7. “Add it later if needed” is not a cheap option. A follow-up spelling costs its own paper, an EWG poll, CWG wording, and a ballot cycle, and risks shipping the operator first and bolting a second spelling on after. Deferral buys no option value. The question is settled here.

13.6 A.6 The wider inventory: what ASCII actually remains

The same availability analysis generalizes, and it gets asked in the room. A sequence XY is mintable only if XY is not a token or token-prefix today and Y cannot validly follow X in a current program. The second clause is the surprising one: after any binary operator or <, the unary-capable characters - + * & ~ ! are already legal, so <-, <+, <*, **, !!, ~~ are all blocked; a * *p and !!x are the cautionary cases. <| survives only because | is the one bar with no unary form.

Free standalone characters: exactly three, as noted in the grammar section. They are the backtick (claimed by this proposal) and @ and $, compromised by Objective-C and -fdollars-in-identifiers respectively. There is a fourth character, \, which is free as a token and is not counted among the three: it is the line-continuation marker and the universal-character-name lead-in, so it is usable only with care.

Clean two-or-more-character sequences of note: ==>, <==, <==>, <|, ~>, and %% are mintable today; => and |> are lexically clean but spoken for, by P2971’s implication operator and P2011’s pipeline respectively; <=> is spaceship; and ^^ — available by the same analysis until recently — was claimed by reflection [P2996R13]. Reflection moved from a single ^ to ^^ in P3381R0 [P3381R0], after running this exercise, and that paper’s candidate table reaches this section’s count independently: it calls the backtick the third character recently added to the basic character set, after $ and @. The lesson from that precedent: doubling an operator with no unary form is the reliable way to find clean real estate, and doubling one that has a unary form never is.

13.7 A.7 Why the demand for that inventory evaporates

This proposal is, in effect, a general infix-operator facility: any named binary operation is x `op` y with no new punctuator. So the standing demand for new operator tokens, which is what previously justified spending scarce lexical real estate, largely evaporates. x `implies` y, x `pow` y, x `dot` y all work today under the feature, and the table in A.6 can stay unspent.

The residual cases where a dedicated punctuator is still worth minting are the ones a desugar-to-call cannot express: non-strict evaluation with a bare-expression right operand (Walter Brown’s short-circuiting => implication [P2971R3]; though the motivation section shows a thunk recovers the capability, so the dedicated operator is an ergonomic win), custom precedence or associativity outside the single backtick level, and operations frequent enough that even `op` is too much ceremony, a high bar. Everything else is a backtick call. The inventory above is what remains technically possible; this proposal removes most of the motivation to spend it.

14 References

[P0543R3] Jens Maurer. 2023-07-19. Saturation arithmetic.
https://wg21.link/p0543r3
[P2011R1] Barry Revzin, Colby Pike. 2020-04-16. A pipeline-rewrite operator.
https://wg21.link/p2011r1
[P2672R0] Barry Revzin. 2022-10-14. Exploring the Design Space for a Pipeline Operator.
https://wg21.link/p2672r0
[P2971R3] Walter E Brown. 2025-01-13. Implication for C++.
https://wg21.link/p2971r3
[P2996R13] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, Dan Katz. 2025-06-20. Reflection for C++26.
https://wg21.link/p2996r13
[P3381R0] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, Dan Katz. 2024-09-17. Syntax for Reflection.
https://wg21.link/p3381r0

  1. A ` that appears within a matching pair of parentheses, brackets, or braces is nested.↩︎