---
title: "P3817R0 — Structured Binding Assignments"
document: P3817R0
date: 2026-07-28
audience: "EWGI SG17: EWG Incubator,EWG Evolution"
reply-to:
  - "Yehonatan Simian <yonisimian@gmail.com>"
  - "Ran Regev <regev.ran@gmail.com>"
---

# Structured Binding Assignments

- Document #: P3817R0
- Date: 2026-07-28
- Project: Programming Language C++
- Audience: EWGI, EWG
- Reply-to:
  - Yehonatan Simian [yonisimian@gmail.com](mailto:yonisimian@gmail.com)
  - Ran Regev [regev.ran@gmail.com](mailto:regev.ran@gmail.com)

## History

R0: Initial version

## Abstract

This proposal introduces an extension to C++ structured bindings, allowing assignment to existing variables.

## Motivation

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::

1. **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 declared`
2. **Unified 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`.
3. **No standard library required** — `std::tie` requires `<tuple>`, which is unavailable in many constrained environments (embedded systems, bare-metal, OS kernels). A language-level feature works everywhere C++ does.
4. **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.”

## Proposal

Extend structured binding syntax to allow the `using` keyword before an element in the *sb-identifier-list* to denote assignment to an already-existing lvalue, rather than declaration of a new variable.

### Syntax

The grammar splits *sb-identifier* into two explicit alternatives:

```cpp
sb-identifier:
    ...opt identifier attribute-specifier-seqopt
    using ...opt unary-expression
```

The first alternative is the existing declaration form, unchanged. The second is new: `using` followed by a *unary-expression* that shall designate a modifiable lvalue. No *attribute-specifier-seq* appears in the second alternative — attributes appertain to a newly declared variable, and no variable is introduced here.

When `...` is present, the `unary-expression` shall be a pack expression; the `...` marks the expansion of that expression, not the introduction of a new pack name.

### Semantics

The hidden variable *e* is introduced exactly as today (per [[dcl.struct.bind] ¶1](https://eel.is/c++draft/dcl.struct.bind)). 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=`.

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:

<!-- tomd:lossy-table -->

```cpp
auto [using x, y] = f()
```

```cpp
auto& [using x, y] = f()
```

```cpp
auto&& [using x, y] = f()
```

#### Illustration

The expansions below are **illustrative** — they show the intended semantics in terms of equivalent code, not normative wording. Types shared across all examples:

```cpp
struct Point { int x, y; };
using PointPair = std::pair<Point, Point>;
PointPair  get_pair();
Point      get_point();
```

<!-- tomd:lossy-table -->

```cpp
Point arr[2];
Point x;
auto [using x, y] = arr;
```

```cpp
Point __e_p3817[2] = {arr[0], arr[1]};
x = std::move(__e_p3817[0]);
Point& y = __e_p3817[1];
```

```cpp
Point arr[2];
Point x;
auto& [using x, y] = arr;
```

```cpp
Point (&__e_p3817)[2] = arr;
x = __e_p3817[0];
Point& y = __e_p3817[1];
```

```cpp
Point arr[2];
Point x;
auto&& [using x, y] = arr;
```

```cpp
Point (&__e_p3817)[2] = arr;
x = __e_p3817[0];
Point& y = __e_p3817[1];
```

```cpp
Point x;
auto [using x, y] = get_pair();
```

```cpp
PointPair __e_p3817 = get_pair();
x = std::get<0>(static_cast<PointPair&&>(__e_p3817));
Point&& y = std::get<1>(static_cast<PointPair&&>(__e_p3817));
```

```cpp
PointPair p;
Point x;
auto& [using x, y] = p;
```

```cpp
PointPair& __e_p3817 = p;
x = std::get<0>(__e_p3817);
Point& y = std::get<1>(__e_p3817);
```

```cpp
PointPair p;
Point x;
auto&& [using x, y] = p;
```

```cpp
PointPair& __e_p3817 = p;
x = std::get<0>(__e_p3817);
Point& y = std::get<1>(__e_p3817);
```

```cpp
Point x;
auto&& [using x, y] = get_pair();
```

```cpp
PointPair&& __e_p3817 = get_pair();
x = std::get<0>(static_cast<PointPair&&>(__e_p3817));
Point&& y = std::get<1>(static_cast<PointPair&&>(__e_p3817));
```

```cpp
int px;
auto [using px, y] = get_point();
```

```cpp
Point __e_p3817 = get_point();
px = std::move(__e_p3817.x);
int& y = __e_p3817.y;
```

```cpp
Point pt;
int px;
auto& [using px, y] = pt;
```

```cpp
Point& __e_p3817 = pt;
px = __e_p3817.x;
int& y = __e_p3817.y;
```

```cpp
Point pt;
int px;
auto&& [using px, y] = pt;
```

```cpp
Point& __e_p3817 = pt;
px = __e_p3817.x;
int& y = __e_p3817.y;
```

```cpp
int px;
auto&& [using px, y] = get_point();
```

```cpp
Point&& __e_p3817 = get_point();
px = std::move(__e_p3817.x);
int& y = __e_p3817.y;
```

> <sup>†</sup> [[dcl.struct.bind] ¶8](https://eel.is/c++draft/dcl.struct.bind) imposes no aggregate requirement: any class type with publicly accessible direct members and no `std::tuple_size` specialization 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.

### Specifiers

#### `const`

A const-qualified structured binding that contains `using`-marked elements is ill-formed.

Example:

```cpp
const auto [using x, y] = ar;  // ill-formed: cannot assign in a const binding
```

**Alternative Considered**

Under this alternative, `const` appertains to the hidden variable *e* — not to the `using`-marked targets — and the declaration is valid. However, this raises a question that defies easy resolution: given

```cpp
Point p, q;
const auto [using p, using q] = get_pair();
```

`p` and `q` are already-declared variables with well-known types. Does this declaration change their types? If yes, that is contrary to C++ semantics — a declaration cannot retroactively change the type of an existing variable. If no, then `const` has no observable effect on the `using`-marked elements, which is misleading.

In practice, `const` would only make *e* const, causing assignments to copy rather than move — a subtle effect invisible in the source.

Note: for types with `mutable` data members, `const` on *e* does not suppress those members — `mutable` members remain modifiable and moveable even through a `const` object. Under this alternative, `const auto [using p, ...]` where the corresponding source member is `mutable` would still produce a move, not a copy. This inconsistency — copying from some elements and moving from others depending on `mutable` — further undermines the predictability of this option.

The authors therefore prefer the ill-formed approach.

#### Storage Class

`static` and `thread_local` are both storage-class specifiers — they declare a new entity with a particular storage duration. `using`-marked elements introduce no new entity; an existing variable already has its own storage class. There is nothing for `static` or `thread_local` to act on in the `using`-marked positions, making their combination with `using` ill-formed:

```cpp
static auto [using x, y] = f();        // ill-formed
thread_local auto [using x, y] = f();  // ill-formed
```

Non-`using` elements are unaffected — `y` above would be a valid static or thread-local binding.

#### `constexpr`

C++26 (P2686R5) makes `constexpr` valid for structured binding declarations for non-`using` elements:

```cpp
constexpr auto [x, y] = get_pair();  // valid in C++26; x and y usable in constant expressions
```

For `using`-marked elements, `constexpr` implies `const` and is therefore ill-formed for the same reason as `const`:

```cpp
constexpr auto [using p, y] = get_pair();  // ill-formed: constexpr implies const
```

#### `constinit`

`constinit` requires static or thread-local storage duration. Since both storage-class specifiers are ill-formed with `using`-marked elements (see Storage Class above), `constinit` with `using` is always ill-formed — no new rule beyond the storage class rule is needed.

### Further Design Decisions

#### Returned Lvalues

The `using` specifier may also appear before an expression yielding an lvalue, not just a plain identifier. This is the language-level equivalent of `std::tie` with non-variable arguments:

```cpp
// std::tie equivalent
std::tie(foo(), s[0]) = get_pair();

// with P3817
auto [using foo(), using s[0]] = get_pair();
```

#### C++26 `_` Placeholder

P3817 composes naturally with C++26’s `_` placeholder for discarding elements:

```cpp
int x;
auto [using x, _] = get_pair();  // assign first element to x, discard second
```

Note: `using _` is ill-formed — `_` is a discard placeholder and cannot be the target of an assignment.

This provides a complete, library-free replacement for `std::tie` with `std::ignore`:

<!-- tomd:lossy-table -->

```cpp
std::tie(x, std::ignore) = get_pair();
```

```cpp
auto [using x, _] = get_pair();
```

```cpp
std::tie(std::ignore, y) = get_pair();
```

```cpp
auto [_, using y] = get_pair();
```

#### Duplicate Variables: ill-formed for assigned elements

Using the same variable more than once in a `using`-marked binding list is ill-formed:

```cpp
int x;
auto [using x, using x] = foo();  // ill-formed
```

Even for types where repeated assignment would be well-defined (e.g., a type whose `operator=` accumulates values), the construct is rejected as inherently confusing.

#### Packs

P3817 composes naturally with C++26 structured binding packs ([P1061](https://wg21.link/P1061)).

**Non-pack `using` alongside a regular pack** requires no special treatment — the two are orthogonal:

```cpp
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 last
```

**`using ...expr`** — when `...` is present after `using`, `expr` shall be a pack expression. Each element of the structured binding is assigned from the corresponding element of *e* to the corresponding expansion of `expr`, following the same ref-qualifier rules as non-pack `using`. No new name is introduced:

```cpp
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:

```cpp
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 existing constraints extend naturally:

- `using ..._` is ill-formed — `_` is a discard placeholder.
- `sizeof...(expr)` must equal the structured binding size of *e* minus the number of non-pack elements; otherwise the program is ill-formed.
- If expanding `expr` produces duplicate lvalue targets, the program is ill-formed (the duplicate variable rule extended to packs).

##### Current Alternatives

Assigning from a tuple-like type to a pack of existing variables without P3817 requires either the standard library or significant boilerplate:

```cpp
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:

```cpp
template <typename... Ts>
void assign_from(std::tuple<Ts...> t, Ts&... targets) {
    auto [using ...targets] = std::move(t);
}
```

## Examples

### Patterns

#### Range-based `for`

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:

```cpp
// 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:

```cpp
Point last{};
for (auto [using last, _] : trajectory) { /* last updated each step */ }
// last is the final point of the trajectory
```

### Real-World Examples

### [llvm PassBuilder](https://github.com/llvm/llvm-project/blob/00062ed982256651a28187e865d6ae14e21d8395/llvm/lib/Passes/PassBuilder.cpp#L766)

```cpp
Expected<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;
}
```

### scylladb: [repair/row_level.cc](https://github.com/scylladb/scylladb/blob/01bb7b629ad97859b7b5f09ab9df4c0c36ad20a2/repair/row_level.cc#L299)

```cpp
- 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);
```

### [tools/scylla-nodetool.cc](https://github.com/scylladb/scylladb/blob/01bb7b629ad97859b7b5f09ab9df4c0c36ad20a2/tools/scylla-nodetool.cc#L2209)

```cpp
- std::tie(params["kn"], params["cf"]) = *split_kt(kn_msg);
+ auto [using params["kn"], using params["cf"]] = *split_kt(kn_msg);
```

## Alternative Syntaxes Considered

### Option A: `&` Symbol

```cpp
int x;
auto [&x, y] = get_values();
```

**Pros:** - Concise - `&` suggests “referencing something that already exists” - Familiar to developers comfortable with reference syntax

**Cons:** - Ambiguous with address-of in expressions like `auto [&get_reference(), y] = ...` - `]]` in `auto [&map[key], y]` resembles attribute syntax - Visual similarity to `auto& [x, y]` may cause initial confusion

### Option B: `using` Keyword *(preferred by authors)*

```cpp
int x;
auto [using x, y] = get_values();
```

**Pros:** - Unambiguous — no confusion with address-of or function pointers - Clear semantic intent: “using” an existing variable rather than declaring a new one - Works cleanly with returned lvalues: `auto [using get_reference(), y] = ...` - Echoes the existing use of `using` to refer to a name declared elsewhere.

**Cons:** - More verbose than `&` - Introduces contextual keyword usage within structured bindings

### Other Syntaxes (Rejected)

- **`=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.
- **Implicit (no specifier)** — Would silently assign or shadow based on scope, violating the principle of least surprise.

## Wording

Will be completed upon positive response on the direction of the proposal.

## Previous Papers

- **P0144R2 — Structured Bindings**: Introduced structured bindings; §3.3 explicitly deferred this extension with an invitation to propose it separately.
- **P2392 — C++ Standard Library Support for Structured Bindings**: Highlights the community’s ongoing interest in extending structured binding utility.

## References

- [P0144R2 - Structured Bindings](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0144r2.pdf)
