R1:
Moved the pack ellipsis to postfix (using expr...
instead of using ...expr), so an expansion may use any
pattern, including a nested pack such as
using arr[Is]...
Specified left-to-right order for using-marked
assignments across all three decomposition kinds
Tightened the “no standard library” motivation into a concrete real-world example (Fuchsia’s Zircon kernel)
Reversed the ill-formed specifier rules: const,
constexpr, constinit and the storage-class
specifiers appertain to the hidden object e and are well-formed
with using-marked elements
Drafted the Wording section, replacing the placeholder: the
change is confined to the using-marked element, and the
specifier rules need no wording at all
Split sb-identifier into two named forms,
sb-declaring-identifier and
sb-assigning-identifier, so the wording can name the kind
of element it means
Duplicate targets are now allowed rather than ill-formed: the ban would not have caught a mistake, and it cannot be enforced
Documented that an assignment target may share a name with a binding the declaration introduces, and that forbidding it is unnecessary
Editorial changes:
Added clang implementation and implementation-tracker references
Restructured the back matter: References is now a bibliography of the cited papers. Runnable examples and implementation links moved into their own section.
Replaced lvalue with modifiable lvalue
where the assignment target is meant
R0:
This proposal introduces an extension to C++ structured bindings, allowing assignment to existing variables.
Structured bindings (auto [x, y] = foo();) can only
declare new variables. Assigning to pre-existing variables requires
std::tie from <tuple>. There is no
single construct that does both.
This proposal closes that gap, with several advantages over std::tie:
Mixed-mode — Neither std::tie nor
structured bindings alone allow some elements to be new variables and
others pre-existing in the same statement. P3817 uniquely enables
this:
int id;
auto [using id, name] = get_record(); // id is assigned; name is declaredUnified syntax — One construct for both
assignment and initialization reduces cognitive overhead and produces
more consistent code. Currently, initialization uses structured
bindings; reassignment uses std::tie.
Standard library use may be restricted, even where
available — Some C++ environments prohibit standard library use
by policy, regardless of whether <tuple> is
technically present.
std::tie is unusable there; a
language-level feature is usable.
For example, Fuchsia’s Zircon kernel disallows standard library use
in-kernel [CxxInZircon].
[PageFault] assigns a loop-scoped variable from a
structured binding to an out-of-loop variable today:
auto [fault_status, _] =
mapping->PageFaultLocked(guest_paddr, pf_flags, 0, &page_request);
status = fault_status;With P3817, this becomes one statement:
auto [using status, _] =
mapping->PageFaultLocked(guest_paddr, pf_flags, 0, &page_request);Encouraged by [P0144R2] — The authors of the original structured bindings paper explicitly invited this extension [section 3.3]:
“This can always be proposed separately later as a pure extension if desired.”
Extend structured binding syntax to allow the using
keyword before an element
in the sb-identifier-list to denote assignment to an
already-existing modifiable lvalue, rather than declaration of a new
variable.
The grammar splits sb-identifier into two named alternatives, so that the wording can name the kind of element it means:
sb-identifier:
sb-declaring-identifier
sb-assigning-identifier
sb-declaring-identifier:
...opt identifier attribute-specifier-seqopt
sb-assigning-identifier:
using unary-expression ...opt
The sb-declaring-identifier is the existing declaration
form, unchanged. The sb-assigning-identifier is new:
using followed by a unary-expression that shall designate a
modifiable lvalue, optionally followed by .... No
attribute-specifier-seq appears in this form: attributes
appertain to a newly declared variable, and no variable is introduced
here.
The two forms place the ellipsis differently on purpose: the
declaring one declares a pack, the assigning one
expands an existing one. C++ spells a declared pack’s ellipsis
on either side of the name (Ts... args,
T ...args, ...rest), but spells every pack
expansion after its pattern (f(args...)); each form follows
the convention of the operation it performs. The two never compete for
the same token, since in the assigning form the ellipsis follows an
expression.
When the assigning form carries ..., the
sb-identifier is a pack expansion ([temp.variadic]) whose
pattern is the unary-expression. The unexpanded pack may appear
anywhere within that pattern — as the pattern itself
(targets) or nested inside it (arr[Is]),
exactly as in any other pack expansion — and each element of the
structured binding is assigned from the corresponding element of
e to the corresponding element of that expansion.
operator= The hidden variable
e is introduced exactly as today (per [dcl.struct.bind]
¶1). The difference lies in how each element SBᵢ is resolved: for a
using-marked element, instead of introducing a new name,
the implementation assigns from the corresponding element of e
to the existing variable via operator=.
Order
of assignment. For all three decomposition kinds
(tuple-like, array, and otherwise/aggregate), the elements SB₀, …, SBₙ₋₁
are resolved in that lexical order: SBᵢ — its assignment if
using-marked, its reference-binding otherwise — is fully
completed before SBᵢ₊₁ is evaluated. In particular, for a
using-marked SBᵢ, the assignment reads its right-hand
element from e and writes it via operator= before
any later SBⱼ (j > i) is evaluated.
Rationale: tuple-like decomposition already has this
ordering “for free” from [dcl.struct.bind]
¶7, since each SBᵢ there is a separately-initialized variable,
sequenced by ordinary declaration semantics. Array and aggregate
decomposition never needed an order, since without using
every SBᵢ is a side-effect-free reference binding. using
adds operator= side effects, making the order observable:
auto& [_, using arr[0], using arr[1]] = arr; intends an
evict-and-shift, but silently corrupts arr under the other
order. This rule generalizes the existing tuple-like guarantee to the
two forms that previously had none.
The ref-qualifier determines the type of e, which in turn determines whether the assignment is a copy or a move — no special-casing is required:
| Declaration | e |
Assignment to using x
|
|---|---|---|
|
owned object (xvalue) | move |
|
lvalue reference | copy |
|
lvalue ref or rvalue ref | copy or move |
The expansions below are illustrative — they show the intended semantics in terms of equivalent code, not normative wording. Types shared across all examples:
struct Point { int x, y; };
using PointPair = std::pair<Point, Point>;
PointPair get_pair();
Point get_point();
auto
|
auto&
|
auto&&
|
|||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Array |
|
|
|
||||||||||||||||
| Tuple-like |
|
|
|
||||||||||||||||
| Otherwise† |
|
|
|
||||||||||||||||
† [dcl.struct.bind] ¶8 imposes no aggregate requirement: any class type with publicly accessible direct members and no
std::tuple_sizespecialization reaches this case, including non-aggregates with user-provided constructors.
Immediate-move implication. For
auto [using x, ...], the move assignment to x
happens at the binding statement, consuming that element of e.
A non-using element does not move — it binds a reference
into e — so moving out of one always takes an explicit
std::move. Where the assignment copies rather than moves,
nothing is consumed.
Specifiers appertain to the hidden object e, never to the
using-marked targets. [dcl.struct.bind] ¶1
defines S as the declaration’s constexpr,
constinit and storage-class specifiers, and e is
defined using S together with the declaration’s cv-qualifiers.
A using-marked element introduces no new entity — it
assigns from the corresponding element of e to an existing
variable, whose type this declaration cannot change. Every specifier
therefore keeps the meaning it has for an ordinary structured binding,
and none of them is ill-formed:
const qualifies e, and constexpr
— valid on structured binding declarations since C++26, [P2686R5] — implies it. The elements read from
e are const, so the assignment to a using-marked
target copies rather than moves.static and thread_local give e
static or thread storage duration. e is initialized once; every
later execution of the statement assigns from that same e —
which earlier iterations may have moved from.constinit constrains the initialization of e,
and volatile qualifies it. Neither changes what a
using-marked element does.The using specifier may also appear before
an expression yielding a modifiable lvalue, not just a plain
identifier. This is the language-level equivalent of
std::tie with non-variable arguments:
// std::tie equivalent
std::tie(foo(), s[0]) = get_pair();
// with P3817
auto [using foo(), using s[0]] = get_pair();_ PlaceholderP3817 composes naturally with C++26’s _ placeholder for discarding elements:
int x;
auto [using x, _] = get_pair(); // assign first element to x, discard secondNote: using _ is ill-formed — _ is a
discard placeholder and cannot be the target of an assignment, even
where _ names an ordinary variable rather than a
placeholder.
This provides a complete, library-free replacement for
std::tie with std::ignore:
std::tie with std::ignore
|
With P3817 and C++26 |
|---|---|
|
|
|
|
A binding list may assign to the same object more than once. The order rule makes the result well-defined — the last assignment wins.
A rule forbidding it was considered and dropped: assigning twice is
not a mistake, only pointless, and no test can enforce it — a test on
the spelling misses two names for one object, and a test on the objects
themselves is undecidable, since using f(), using g() may
return the same object:
int var;
int& x = var;
int& y = var;
auto [using x, using y] = foo(); // x and y designate the same objectAn assignment target may share its name with a binding the declaration introduces. The operand finds whatever the name means outside the list, since the bindings are not in scope inside it:
struct Counter {
int count = 0; // the member
std::pair<int, int> tick(); // {delta, running_total}
int step() {
auto [count, using count] = tick();
// │ │
// │ └── this `count` is the MEMBER: at this point the
// │ binding declared to its left is not yet in scope
// └── declares a local binding named `count`
use(count); // the binding — first element (delta)
return this->count; // the member — second element (running_total)
}
};The two cases that would warrant a rule are already ill-formed: redeclaring a name that exists in the same scope, and naming a binding that is not yet in scope — which is simply an undeclared identifier. What is left is the deliberate reuse above, whose meaning follows from the ordinary scope rules and whose behaviour is well-defined, so forbidding it is unnecessary.
P3817 composes naturally with C++26 structured binding packs [P1061R10].
Non-pack using alongside a regular pack
requires no special treatment — the two are orthogonal:
int x;
auto [using x, ...rest] = get_tuple(); // x is assigned; rest is a new binding pack
auto [...rest, using x] = get_tuple(); // rest is a new binding pack; x is assigned lastusing expr... — when ...
follows the unary-expression, the sb-identifier is a
pack expansion whose pattern is that expression. Each element of the
structured binding is assigned from the corresponding element of
e to the corresponding element of the expansion, following the
same ref-qualifier rules as non-pack using. No new name is
introduced:
template <typename... Ts>
void assign_from(std::tuple<Ts...> t, Ts&... targets) {
auto [using targets...] = std::move(t); // each tuple element moved into the corresponding target
}Mixed usage is also valid:
template <typename T, typename... Rest>
void assign_all(T& head, Rest&... tail, std::tuple<T, Rest...> t) {
auto [using head, using tail...] = std::move(t);
}The operand may be any expression, not only a name — an unexpanded pack may appear nested inside it, as in any other pack expansion:
template <typename... Ts, std::size_t... Is>
void assign_into(std::tuple<Ts...> t, std::array<std::common_type_t<Ts...>, sizeof...(Is)>& arr,
std::index_sequence<Is...>) {
auto [using arr[Is]...] = std::move(t); // each tuple element moved into the corresponding arr[Is]
}More than one expansion is allowed — any number of
using expansions may appear in one binding list, and a
declaring pack may appear alongside them. Nothing is left to infer: each
expansion’s length is fixed by the pack named in its pattern, so only a
declaring pack’s size comes out as the remainder, and the C++26 rule
that a declaration contains at most one declaring pack ([P1061R10]) is what keeps that remainder
well-defined.
auto [using a..., using b...] = get_tuple(); // |a| + |b| must be the number of elements of the tupleThe existing constraints extend naturally:
using _... is ill-formed — _ is a discard
placeholder.Assigning from a tuple-like type to a pack of existing variables without P3817 requires either the standard library or significant boilerplate:
template <typename... Ts>
void assign_from(std::tuple<Ts...> t, Ts&... targets) {
// Option 1: std::tie — requires <tuple>
std::tie(targets...) = std::move(t);
// Option 2: index sequence — verbose
[&]<std::size_t... Is>(std::index_sequence<Is...>) {
int dummy[] = { (targets = std::get<Is>(std::move(t)), 0)... };
(void)dummy;
}(std::index_sequence_for<Ts...>{});
}With P3817:
template <typename... Ts>
void assign_from(std::tuple<Ts...> t, Ts&... targets) {
auto [using targets...] = std::move(t);
}Because the structured binding declaration fires on every iteration, using-marked elements are reassigned each time. This enables tracking state across iterations without a separate assignment in the loop body:
// Returns {head, tail} split at the first delimiter
std::pair<std::string_view, std::string_view>
split_first(std::string_view s, char delim);
std::string_view remaining = input;
while (!remaining.empty()) {
auto [token, using remaining] = split_first(remaining, ' ');
process(token);
}Mixed-mode is equally natural — accumulate one variable while binding fresh names for the rest:
struct Point { double x, y; };
std::vector<std::pair<Point, double>> trajectory; // {position, timestamp}
Point last{};
for (auto [using last, _] : trajectory) { /* last updated each step */ }
// last is the final point of the trajectoryExpected<bool> PassBuilder::parseSinglePassOption(StringRef Params,
StringRef OptionName,
StringRef PassName) {
bool Result = false;
while (!Params.empty()) {
- StringRef ParamName;
- std::tie(ParamName, Params) = Params.split(';');
+ auto [ParamName, using Params] = Params.split(';');
if (ParamName == OptionName) {
Result = true;
} else {
return make_error<StringError>(
formatv("invalid {} pass parameter '{}'", PassName, ParamName).str(),
inconvertibleErrorCode());
}
}
return Result;
}- mutation_reader rd(nullptr);
- std::tie(rd, _reader_handle) = make_manually_paused_evictable_reader(
+ auto [rd, using _reader_handle] = make_manually_paused_evictable_reader(
std::move(ms),
_schema,
_permit,
_range,
_schema->full_slice(),
{},
mutation_reader::forwarding::no);- std::tie(params["kn"], params["cf"]) = *split_kt(kn_msg);
+ auto [using params["kn"], using params["cf"]] = *split_kt(kn_msg);& Symbolint x;
auto [&x, y] = get_values();Pros:
& suggests “referencing something that already
exists”Cons:
auto [&get_reference(), y] = ...]] in auto [&map[key], y] resembles
attribute syntaxauto& [x, y] may cause initial
confusionusing Keyword (preferred by authors)int x;
auto [using x, y] = get_values();Pros:
auto [using get_reference(), y] = ...using to refer to a name
declared elsewhere.Cons:
&=x — Conflicts with lambda
capture-by-value intuition ([=]).let — Conflicts with Pattern Matching
proposals.tie x — Overly verbose; confusingly
evokes std::tie even though it does not use it.The wording below is relative to [N5054].
Changes are marked as insertions and deletions. Paragraph
numbers are those of [N5054]; an inserted
paragraph shifts the numbering of the paragraphs that follow it, and no
renumbering is shown. It is a draft for reviewing the design as
specifiable text, not a request to apply changes at this stage.
Replace the production for sb-identifier:
sb-identifier:
sb-declaring-identifier
sb-assigning-identifier
sb-declaring-identifier:
...opt identifier attribute-specifier-seqopt
sb-assigning-identifier:
using unary-expression ...opt
[1]
[2]
[3]
Insert after ¶3:
An sb-assigning-identifier introduces no names and is not a structured binding. If it is not a pack sb-identifier, the object designated by its unary-expression is its assignment target, and that unary-expression shall be a modifiable lvalue. If it is a pack sb-identifier, the pattern designates one assignment target per element of the expansion, and each of them shall be a modifiable lvalue.
For each SBᵢ that is not a structured binding, the assignment
expression target = element is evaluated, where
target is the assignment target of SBᵢ and element is
the object or value that corresponds to SBᵢ — the element i of
the array in the array case, the initializer in the tuple-like
case, or e.mᵢ in the member case.
An SBᵢ that is a structured binding is resolved by introducing it; any other SBᵢ is resolved by evaluating its assignment expression. The initialization of e, and any conversion of e considered as a decision variable ([stmt.pre]), is sequenced before the resolution of SB₀, and the resolution of SBᵢ is sequenced before the resolution of SBᵢ₊₁.
Insert after ¶5:
If the unary-expression of an
sb-assigning-identifier is the identifier _, the
program is ill-formed.
[6], [7], [8]
S Uᵢ rᵢ = initializer ; — and “Each SBᵢ that is a
structured binding is the name of an lvalue of type Tᵢ that refers
to the object bound to rᵢ; the referenced type is Tᵢ.”e.mᵢ; …”In ¶6, add to the list of contexts in which a pack expansion can occur:
The examples in this paper run against a clang implementation of
P3817. A snippet that is cross-referenced as [‡ N] links to
the numbered row below; every row points to a runnable Godbolt page, in
the order the paper presents them.
| ‡ | Description | Link |
|---|---|---|
| 1 | Motivation — mixed-mode: one element assigned, one declared | godbolt.org/z/KhEfTYsWo |
| 2 |
Motivation — one statement instead of two (Zircon
PageFault)
|
godbolt.org/z/v11xWEWrr |
| 3 |
Semantics — ref-qualifier table, auto on f()
|
godbolt.org/z/PeGcnMWP8 |
| 4 |
Semantics — ref-qualifier table, auto& on
f()
|
godbolt.org/z/fxvMY6qo6 |
| 5 |
Semantics — ref-qualifier table, auto&& on
f()
|
godbolt.org/z/7vnEjK4nb |
| 6 |
Semantics — array, auto
|
godbolt.org/z/Whe8YWz83 |
| 7 |
Semantics — array, auto&
|
godbolt.org/z/56oYPaGqf |
| 8 |
Semantics — array, auto&&
|
godbolt.org/z/3dzzb755x |
| 9 |
Semantics — tuple-like, auto
|
godbolt.org/z/E6M9Yf6oK |
| 10 |
Semantics — tuple-like, auto&
|
godbolt.org/z/1hWaKdG75 |
| 11 |
Semantics — tuple-like, auto&& (lvalue)
|
godbolt.org/z/8d6sW38Mx |
| 12 |
Semantics — tuple-like, auto&& (rvalue)
|
godbolt.org/z/bbThTbcvo |
| 13 |
Semantics — member decomposition, auto
|
godbolt.org/z/evvzxvM6o |
| 14 |
Semantics — member decomposition, auto&
|
godbolt.org/z/e1c8EvxYe |
| 15 |
Semantics — member decomposition, auto&& (lvalue)
|
godbolt.org/z/7anj3MddP |
| 16 |
Semantics — member decomposition, auto&& (rvalue)
|
godbolt.org/z/Ws1G1PYYP |
| 17 |
Returned Modifiable Lvalues — using foo(),
using s[0]
|
godbolt.org/z/9qejoMM1P |
| 18 |
C++26 _ placeholder — assign first, discard second
|
godbolt.org/z/sz1Tnebq8 |
| 19 |
C++26 _ placeholder — std::tie(x, std::ignore)
equivalent
|
godbolt.org/z/8EhKrYqfb |
| 20 |
C++26 _ placeholder — std::tie(std::ignore, y)
equivalent
|
godbolt.org/z/3fhKc5h35 |
| 21 |
Packs — non-pack using alongside a regular pack
|
godbolt.org/z/h8dzGsbz7 |
| 22 |
Packs — using targets...
|
godbolt.org/z/f3e1Tvfz6 |
| 23 |
Packs — mixed using head, using tail...
|
godbolt.org/z/5jv7vdMT1 |
| 24 |
Packs — using arr[Is]..., driven by an index pack
|
godbolt.org/z/cr1j8hxxG |
| 25 | Packs — Current Alternatives, rewritten with P3817 | godbolt.org/z/GTq75Kv49 |
| 26 | Examples — Loops | godbolt.org/z/rYW4jsYTP |
| 27 | Examples — Loops, accumulating into one variable | godbolt.org/z/cG4z1nWxr |
| 28 |
Alternative Syntaxes — Option B (using)
|
godbolt.org/z/h1843enhz |
Implementation and ongoing work:
std in kernel”.zircon/kernel/hypervisor/aspace.cc.