Last Update:
Structured bindings in C++17, 8 years later

Structured binding is a C++17 feature that allows you to bind multiple variables to the elements of a structured object, such as a tuple or struct. This can make your code more concise and easier to read, especially when working with complex data structures. On this blog, we already covered this functionality, but we’ll talk about some good C++26 additions and real code use cases.
Iterating through maps
An excellent demonstration of structured bindings is an iteration through a map object.
If you have a std::map
of elements, you might know that internally, they are stored as pairs of <const Key, ValueType>
.
Now, when you iterate through elements, you can do:
for (const auto& elem : myMap) { ... }
You need to write elem.first
and elem.second
to refer to the key and the value.
std::map<KeyType, ValueType> myMap = getMap();
// C++14:
for (const auto& elem : myMap) {
// elem.first - is the key
// elem.second - is the value
}
One of the coolest use cases of structured binding is that we can use it inside a range-based for loop:
// C++17:
for (const auto& [key,val] : myMap) {
// use key/value directly
}
In the above example, we bind to a pair of [key, val]
so we can use those names in the loop. Before C++17, you had to operate on an iterator from the map - which returns a pair <first, second>
. Using the real names key/value
is more expressive.
The above technique can be used in the following example:
#include <map>
#include <iostream>
int main() {
const std::map<std::string, int> mapCityPopulation {
{ "Beijing", 21'707'000 },
{ "London", 8'787'892 },
{ "New York", 8'622'698 }
};
for (const auto&[city, population] : mapCityPopulation)
std::cout << city << ": " << population << '\n';
}
In the loop body, you can safely use the city
and population
variables.
The Syntax
The basic syntax for structured bindings is as follows:
auto [a, b, c, ...] = expression;
auto [a, b, c, ...] { expression };
auto [a, b, c, ...] ( expression );
The compiler introduces all identifiers from the a, b, c, ...
list as names in the surrounding scope and binds them to sub-objects or elements of the object denoted by the expression.
Behind the scenes, the compiler might generate the following pseudo code:
auto tempTuple = expression;
using a = tempTuple.first;
using b = tempTuple.second;
using c = tempTuple.third;
Conceptually, the expression is copied into a tuple-like object (tempTuple
) with member variables that are exposed through a
, b
and c
. However, the variables a
, b
, and c
are not references; they are aliases (or bindings) to the generated object member variables. The temporary object has a unique name assigned by the compiler.
For example:
std::pair a(0, 1.0f);
auto [x, y] = a;
x
binds to int
stored in the generated object that is a copy of a
. And similarly, y
binds to float
.
I don’t want to repeat myself, so please see this blog post - Structured bindings in C++17, 5 years later - C++ Stories, or my book - C++17 in Detail for details about bindings.
Real code
Let’s have a look at some code in the real projects. I found the following use cases:
From chars handling
onnxruntime/migraphx_execution_provider_utils.h
inline int ToInteger(const std::string_view sv) {
int result = 0;
if (auto [_, ec] = std::from_chars(sv.data(), sv.data() + sv.length(), result); ec == std::errc()) {
return result;
}
ORT_THROW("invalid input for conversion to integer");
}
Here, std::from_chars
returns a std::from_chars_result
with two fields: ptr
and ec
.
The code only cares about the error code, so structured bindings make it easy to ignore the pointer (_
) and directly test ec
.
Read more about from_chars
here: C++ String Conversion: Exploring std::from_chars in C++17 to C++26 - C++ Stories
Unmap
static void UnmapFile(void* addr, size_t len) noexcept {
int ret = munmap(addr, len);
if (ret != 0) {
auto [err_no, err_msg] = GetErrnoInfo();
LOGS_DEFAULT(ERROR) << "munmap failed. error code: " << err_no << " error msg: " << err_msg;
}
}
GetErrnoInfo()
returns both an error number and a message. Using structured bindings makes the call-site clean and self-documenting, with no extra boilerplate for std::pair
access (.first
, .second
).
Softmax
auto [scale_tensor, zero_tensor] = GetQuantizationZeroPointAndScale(graph, node_unit.Outputs()[0]);
Initializer q_scale(graph.GetGraph(), *scale_tensor, node_unit.ModelPath());
if (fabs(q_scale.DataAsSpan<float>()[0] - 1.0f / 256.0f) > 0.0001f) {
break;
Here, a helper function returns both the quantization scale and zero-point tensors. Structured bindings allow unpacking them directly into named variables, instead of handling them as std::pair
elements or temporary structs.
Iteration
onnxruntime/prepacked_weights_container.h
size_t GetNumberOfKeyedBlobsForWriting() const noexcept {
size_t result = 0;
for (const auto& [_, keys] : weight_prepacks_for_saving_) {
result += keys.size();
}
return result;
The standard use case for structured bindings and iteration over a map.
Hash computation
openvinotoolkit/compilation_context.cpp
for (const auto& [name, option] : compileOptions) {
seed = hash_combine(seed, name + option.as<std::string>());
}
Another use case for iteration over a container.
C++17/C++20 changes
During the work on C++20, there were several proposals that improved the initial support for structured bindings, most of them are added as defect reports (DR) against C++17:
Relaxing the structured bindings customization point finding rules - P0961
The proposal relaxes the rule so that only member template get<I>
functions are considered, while plain get()
members no longer interfere, making structured bindings more flexible and consistent.
Allow structured bindings to accessible members - P0969
P0969 fixed an odd inconsistency in C++17: structured bindings could only bind to public data members (or members of public bases), even if the member was actually accessible in the current scope. This meant that code which could access a private member via friendship, inheritance, or even within the class itself, failed when using structured bindings. C++20 relaxed this rule so that structured bindings follow the normal accessibility rules of C++: if you can name the member in that scope, you can also bind to it. This made structured bindings consistent with all other forms of member access.
Extending structured bindings to be more like variable declarations - P1091
P1091R3 made structured bindings feel more like “real” variable declarations in C++20. In C++17 they were restricted and somewhat “magical”: you couldn’t mark them static
, thread_local
, or capture them in lambdas. This proposal removed those inconsistencies — now structured bindings can carry storage-class specifiers, be captured by value in lambdas, and behave consistently with ordinary variables. It brought them closer to being true first-class citizens in the language.
#include <tuple>
#include <iostream>
int main() {
static auto [x, y] = std::tuple{10, 20};
auto lambda = [=] {
std::cout << "x = " << x << ", y = " << y << "\n";
};
lambda();
}
Reference capture of structured bindings - P1381
The previous proposal allowed lambdas to capture structyred bindings by value, and now the missing part is to capture by reference:
struct Foo { int x; int y; };
int main() {
Foo foo{10, 20};
auto [a, b] = foo;
auto by_val = [=] { return a + b; };
auto by_ref = [&] { return a + b; };
return by_ref(); // 30
}
C++26 Updates
In C++26, we have at least four new features that went into the Standard:
Attributes for structured bindings - P0609R3
There are not many sensible attributes that can be applied to variables, but we can now try maybe_unused
attribute on a part of structured binding:
std::pair xy { 42.3, 100.1 };
auto [x, y [[maybe_unused]]] = xy;
std::print("{}", x);
The interesting thing is that the attribute is applied after the name of the binding.
Structured binding declaration as a condition - P0963R3
This paper proposes to allow structured binding declarations with initializers appearing in place of the conditions in if, while, for, and switch statements.
std::pair xy { 42.3, 100.1 };
if (auto [x, y] = xy; x > 40.0)
std::print("x is larger than 40");
Structured bindings can introduce a pack - P1061R10
This is a really powerful feature!
Before C++26, structured bindings required you to write all identifiers explicitly (auto [x, y, z] = ...;)
. Converting a tuple to a pack is awkward—you need std::apply
or index_sequence
. But now structured bindings can introduce a pack using ...identifier
. This gives you a parameter-pack-like expansion directly from a tuple/array/aggregate inside a structured binding.
In short:
auto [head, ...rest] = std::tuple{1,2,3,4};
// head = 1, rest... expands to (2,3,4)
Here’s a super cool (and mind-boggling) example:
// Generic dot product for any tuple-like / array / aggregate supported by structured bindings.
template <class P, class Q>
constexpr auto dot_product(const P& p, const Q& q) {
// Bind all elements of each input into packs.
const auto& [...ps] = p;
const auto& [...qs] = q;
// (Optional) sanity check: both must have the same size
static_assert(sizeof...(ps) == sizeof...(qs), "Mismatched sizes");
// Elementwise multiply, then fold with +
return ((ps * qs) + ...);
}
constexpr std::array<int, 3> a{1, 2, 3};
constexpr std::array<int, 3> b{4, 5, 6};
static_assert(dot_product(a, b) == 32);
Here’s the link to Compiler Explorer
constexpr structured bindings and references to constexpr variables - P2686R5
int main() {
constexpr auto [a, b] = std::tuple{2, 3};
static_assert(a * b == 6);
}
Unfortunately, as of Sept 2025, no compiler supports this.
Summary
In this text, we explored structured bindings and how they evolved over the recent C++ Standards. This feature was a big “magic” and used some compiler tricks to work, which also created some restrictions. Now (especially in C++26), those bindings can be used almost like standard variables.
Back to you
- Do you use structured bindings?
- What are the most common use cases for you?
Share your feedback in the comments below.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: