| Document #: | P4307R0 [Latest] [Status] |
| Date: | 2026-09-22 |
| Project: | Programming Language C++ |
| Audience: |
EWG |
| Reply-to: |
Steve Downey <sdowney@gmail.com> |
|>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-associativex `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.
Before
|
After
|
|---|---|
|
|
|
|
|
|
|
|
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.
Before
|
After
|
|---|---|
|
|
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.
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.
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.
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 holdsThe 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 errorThe 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.
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 orderThe 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.
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.
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.
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.
|>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:
x `f` y
desugars to
f(x, y):
symmetric binary infix application of a callable. The callee sits
between two operands, and the result is an ordinary overload-resolved
call.|>.
x |> f(a, b)
is rewritten to
f(x, a, b):
a syntactic rewrite that prepends the left operand to the call written
on its right. There is no
operator|>;
it is not overloadable, by design, and the right-hand call has any
arity.
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:
x |> f(a, b, c)
prepends x to an arbitrary-arity
call; backtick’s right-hand side is a single operand, not an argument
list, so there is no backtick spelling of
f(x, a, b, c).
Beyond two operands, only
|>
threads.|>
cannot write an operation between its operands.
a `min` b
becomes
a |> min(b),
which reads as a pipeline stage. For
x op y notation (predicates,
arithmetic, metrics), backtick is the spelling.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.
Both uses have shipped elsewhere, repeatedly. Neither is invented here; the design work in this paper is fitting them into C++’s grammar.
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.
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.
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.
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.
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.
a `f` b `g` c // g(f(a, b), c)Chains group in reading order, like
- and
/. Nothing
more to it.
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.
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.
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.
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.
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.
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.
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.
* 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.
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.
user-infix-expression:
cast-expression
user-infix-expression ` backtick-operator ` cast-expression
backtick-operator:
assignment-expression
simple-type-specifier
typename-specifierThe 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
::.
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.
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 slotAn 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.
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.
x `f` y
must not have weaker lookup than
f(x, y).
Neither compiler delivered that on its first attempt, both for the same
reason, and ADL fidelity is now a normative rule of the design rather
than an expected consequence of it.`new`;
the AST dump keeps the bare word. Each compiler had to decide which of
its printing surfaces name an entity, and each got the line wrong
once.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:
int `int` = 0;
for two months).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.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.
Both forks are public:
CallExpr, the driver flag, a
transparent AST wrapper so
-ast-print
round-trips the surface syntax, and clang-format support for both uses
(canonical spacing, and a break policy that hard-forbids a break next to
either backtick). A bare unqualified name in the slot reaches the call
builder unresolved, as an
UnresolvedLookupExpr, so ADL is the
call’s. Tests live under
clang/test/
(the
backtick-*
files in
Parser/,
Lexer/,
SemaCXX/,
AST/,
Analysis/
and
CIR/CodeGen/,
plus Driver/
and the Format unit tests), and the full
check-clang
regression gate stays green with the flag off and on.libcpp token
(replacing today’s
stray '`' in program
diagnostic), parser precedence level and slot handling, desugaring via
finish_call_expr, the same flag, and
ADL on the slot for both unqualified forms, a plain name and a
template-id. Tests live under
gcc/testsuite/g++.dg/backtick/,
with a module pair under
g++.dg/modules/
checking that an escaped name streams through a compiled module
interface and mangles as the ordinary module-attached name it is.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.
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.
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.
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.
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.
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.
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.
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.
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.
x `f` y
is
f(x, y).
It is desugared in the front end, before overload resolution, to the
call node the compiler would have built for the call; overload
resolution, ADL, templates,
constexpr,
conversions, value categories and code generation are the call’s. ADL
fidelity is normative: the slot reaches the call builder
unresolved.x `T` y
is
T(x, y)
and CTAD applies. The type reading wins when lookup finds a type or a
class template. (Clang implements the type slot; GCC does not yet.)*, looser
than the unary and postfix operators. Operands are
cast-expressions, so a prefix operator binds to its own operand
and
-a `f` -b
is
f(-a, -b).a `f` b `g` c
is
g(f(a, b), c).
Fixity is fixed, not declared per name and not derived from a
spelling.The keyword escape.
`kw`,
wherever the grammar uses identifier as a terminal, is an
ordinary identifier spelled kw.
Lookup, overload resolution, mangling, linkage and ABI are untouched;
void `new`();
links as a function named
new.Shared.
pipe,
then,
mbind and
implies are motivation, each a few
lines of user code, and any that deserve standardizing arrive in a later
paper with usage behind them.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.
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.
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 ]
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]
Modify the grammar of 7.6.4 Pointer-to-member operators [expr.mptr.oper] paragraph 1:
pm-expression:cast-expressionuser-infix-expression pm-expression .*cast-expressionuser-infix-expression pm-expression ->*cast-expressionuser-infix-expression
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-specifier1 An expression of the form
E1 `O` E2is identical (by definition) toO(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
Tor a placeholder for a deduced class type, an expression of the formE1 `T` E2is identical (by definition) toT(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.
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.
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.
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:
<, a
unary-capable character is already valid:
a < -b,
a * *p,
!!x. Minting
the two-character form silently changes meaning.::
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.\uXXXX can begin an identifier, so a
candidate whose second character is
\ can split a UCN, a non-obvious
break.
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 |
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.
-ast-print
must choose; grep, linters, and tooling grow a second case, forever, for
a cosmetic win.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.
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.
A ` that
appears within a matching pair of parentheses, brackets, or braces is
nested.↩︎