Skip to content
VLSI Mentor

C++ Part I · Module 9 · Arrays, Strings, and Aggregate Data

Structured Bindings

Every example since Chapter 9.7 has reached into a struct one dot at a time — r.field.width, p->field.offset — and that is correct, readable code which becomes noise in one specific situation. A function that returns a group of two or three related values hands the caller a single object, and the caller almost always wants the members rather than the object: it names a local variable for the whole thing, then immediately writes that name three times to get at what it actually came for. C++17 added a declaration form that skips the middle step and gives the members their own names directly. The first half of this chapter is that form, which is small, and the diagnostics it produces when you get the member count wrong, which are good. The second half is the part that actually matters and the reason the frozen purpose names it explicitly: writing auto here means the same thing it meant in a range-based for loop in Chapter 5.6, which is that you get copies. Measured with the allocation counter from Chapter 9.6, the copying form allocated and both reference forms did not — and when the binding is a copy, writing through one of those names changes nothing, silently.

Intermediate16 min readC++Part IFundamentalsC++17

Part I · Module 9 · Chapter 9.10 · Structured Bindings

1. The Noise This Removes

Chapter 6.3 established that a function returning several related values should return a struct rather than use output parameters. That is right, and here is what the caller then writes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Reading r = latest();
    std::cout << r.cycle << ' ' << r.value << '\n';

The variable r exists for one line and is never wanted as a whole. What the caller came for is a cycle and a value. C++17 lets it say so:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    auto [cycle, value] = latest();
    std::cout << cycle << ' ' << value << '\n';

A structured binding declares names for an object's members instead of a name for the object.

That is the whole feature, and §5 is where the care is needed.

2. The Form

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    auto [name1, name2, name3] = expression;

The names in brackets are matched positionally to the members, in declaration order — the same rule Chapter 9.8 §3 gave for brace initialization, running the other way. There is no matching by name: the first name gets the first member, whatever either is called.

It works on three things:

A struct whose members are accessible — the usual case, and class types with public members too.

An array of known length — measured, with int arr[3]:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int arr[3] = {1, 2, 3};
    auto [x, y, z] = arr;          // compiles clean; x=1 y=2 z=3

A std::pair or std::tuple — library types from Module 19. A std::pair holds exactly two values and is what a std::map stores, which is why the canonical example is the one in the index:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    auto [key, value] = *it;       // *it is a std::pair

This chapter borrows std::pair and std::map for §3 and §8 without teaching them, because they are where the feature is most used and the frozen annotation names them.

3. All Four Forms, Measured

Azvya Education Pvt. Ltd.VLSI Mentor
bind.cpp — complete program
#include <iostream>
#include <map>
#include <string>
#include <utility>

struct Reading { int cycle; double value; };

Reading latest() { return Reading{42, 3.5}; }

int main()
{
    auto [cycle, value] = latest();
    std::cout << "by value from a function: " << cycle << ", " << value << '\n';

    Reading r{1, 2.0};

    auto [c1, v1] = r;          // a COPY
    c1 = 99;
    std::cout << "after writing through a copy,      r.cycle = " << r.cycle << '\n';

    auto& [c2, v2] = r;         // a REFERENCE
    c2 = 99;
    std::cout << "after writing through a reference, r.cycle = " << r.cycle << '\n';

    std::pair<std::string, int> entry{"width", 8};
    const auto& [key, val] = entry;
    std::cout << "a pair: " << key << " = " << val << '\n';

    std::map<std::string, int> widths{{"addr", 32}, {"data", 64}};
    for (const auto& [name, bits] : widths)
        std::cout << "  " << name << " is " << bits << " bits\n";

    return 0;
}

Measured output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
by value from a function: 42, 3.5
after writing through a copy,      r.cycle = 1
after writing through a reference, r.cycle = 99
a pair: width = 8
  addr is 32 bits
  data is 64 bits

Line 2 and line 3 are the chapter. Same syntax but for one &, and writing 99 either reached the original object or vanished.

And the last three lines are the form you will write most: a range-based for over a map, where const auto& [name, bits] names the key and the value instead of leaving you to remember which of .first and .second is which.

4. The Diagnostics Are Good

Three mistakes, each measured on its own:

Wrong count. Both directions are caught, and the message is unusually direct:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
auto [a]     = r;   error: type 'Reading' decomposes into 2 elements,
                           but only 1 name was provided
auto [a,b,c] = r;   error: type 'Reading' decomposes into 2 elements,
                           but 3 names were provided

This is a real benefit over r.cycle: if a member is added to the struct, every structured binding of it stops compiling and you are told where. That is the same maintenance property Chapter 9.9 §8 got from -Wswitch, from a different direction.

Inaccessible members. You cannot decompose a type whose members are private — measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: cannot decompose private member 'a' of 'Hidden'

Which is the correct refusal: a type with private members has decided its members are not the interface, and Module 11 is where that decision is made. Structured bindings are for aggregates, which Chapter 9.8 §2 defined as types that are just their members.

Assigning through a const binding is refused as you would expect:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
const auto [a,b] = r;  a = 5;
    error: cannot assign to variable 'a' with const-qualified type 'const int'

One limitation worth knowing because it surprises people: you cannot ignore a member. There is no placeholder — you must name all of them. Writing auto [a, _] = r; compiles, but _ is an ordinary variable name, not a wildcard.

The compensation is that an unused binding is treated as one entity rather than several, which is visible in the message — measured with neither name used:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: unused variable '[a, b]'

It names '[a, b]', not a and b separately. Measured, that cuts both ways:

CodeDiagnostic
auto [a, b] = r; — neither usedwarning: unused variable '[a, b]'
auto [a, b] = r; a = 5; — one usedclean
auto [a, b] = r; std::cout << b; — the other usedclean

So naming a member you do not use costs you nothing — no warning, no (void) cast needed — which is what makes the missing placeholder a small inconvenience rather than a real problem. The price is that you also lose per-member unused detection: the compiler will not tell you that one of the three names you wrote was never read.

5. The Half That Bites: auto Copies

This is Chapter 5.6's decision, in new syntax, with the same consequence.

Chapter 5.6 measured that for (auto x : v) copies each element and for (auto& x : v) does not, and that writing through the copy changes nothing. Structured bindings inherit exactly that, because the auto in front of the brackets qualifies the hidden object the names refer to:

WrittenThe members nameWriting through a name
auto [a, b] = r;copieschanges the copy only
auto& [a, b] = r;the original memberschanges r
const auto& [a, b] = r;the original members, read-onlyrefused at compile time

§3's measurement is the proof: c1 = 99 left r.cycle at 1, and c2 = 99 set it to 99.

The & is easy to miss, and the failure is silent. There is no diagnostic for writing through a copy — it is a perfectly legal thing to do, and if your intent was to modify the original then nothing in the code says so.

6. And the Copy Has a Cost

Chapter 9.6 §4 built an allocation counter by replacing operator new. The same tool answers the question here. The struct holds a std::string with a 40-character value — past the 22-character inline capacity measured in Chapter 9.6 §4, so a copy of it must allocate:

Azvya Education Pvt. Ltd.VLSI Mentor
cost.cpp — the relevant part
struct Signal { std::string name; int width; };

Signal s{"a_deliberately_long_signal_name_past_sso", 32};

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
construction took 1 allocation(s)
auto [n,w]        = s : 1 allocation(s)
const auto& [n,w] = s : 0 allocation(s)
auto& [n,w]       = s : 0 allocation(s)

The copying form allocated; neither reference form did. One & is the difference between a heap allocation and none, in a declaration whose purpose was readability.

And the Chapter 9.6 qualification applies here too, for the same reason: with a short name the count would have been 0 for every row, because the small-string optimization would have absorbed it. The cost is real and it is invisible in a small test.

7. It Is a Declaration, So Scope Applies

Nothing special here, but worth saying because the brackets make it look like a new kind of statement. A structured binding is a declaration, so everything in Module 7 applies unchanged: the names are block-scoped, they shadow outer names of the same name per Chapter 7.1 §6, and they live until the end of the enclosing block per Chapter 7.2.

Two consequences:

You cannot declare them and assign later. There is no auto [a, b]; — a structured binding must be initialized, like a reference.

They work in an if with an initializer, which is Chapter 5.1 §8's form:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    if (auto [it, inserted] = m.insert({"addr", 32}); inserted) {
        // the names are scoped to the if, and `inserted` is the condition
    }

That combination — C++17's if initializer with C++17's structured bindings — is the idiomatic way to call a function that returns both a result and a status, and it keeps both names out of the enclosing scope.

8. The C++ Version, Measured

Structured bindings are C++17. Compiling the same file at -std=c++14 produced, four times:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: decomposition declarations are a C++17 extension [-Wc++17-extensions]

A warning rather than an error because clang implements it as an extension; a different compiler or -pedantic-errors may reject it. Chapter 1.3 §3 fixed this curriculum at C++17, so the feature is available throughout, but it is the kind of thing to check before using it in an existing codebase.

The pre-C++17 way to do roughly this was std::tie:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int cycle; double value;
    std::tie(cycle, value) = someTuple;      // the old form

which required declaring the variables first, so it could not deduce their types and could not bind to a plain struct at all. You will meet it in older code. It is named here, not taught.

9. What to Actually Do

Use a structured binding when you want a function's returned members and not the object — it is exactly Chapter 6.3's return-a-struct advice made comfortable at the call site.

Use const auto& [a, b] in a range-based for, always, unless you are modifying elements.

Use plain auto when binding a function's return value — there is nothing to copy twice.

Use auto& when you intend to modify the original, and check for the & whenever a write seems not to take effect.

Prefer it to .first and .second when iterating a map. [name, bits] says what they are; .second does not.

Do not use it on a type with private members — it is refused, correctly.

Remember you must name every member. There is no placeholder — but an unused name costs nothing, because the warning covers the whole binding.

Treat a sudden compile error in every binding of a struct as information, not an obstacle: a member was added, and these are the places that need to know.

10. Mistakes Worth Making Once

Writing auto [a, b] and then modifying a, expecting the original to change. Measured: r.cycle stayed at 1. No diagnostic.

Writing auto instead of const auto& in a loop. Measured: one allocation per bind where there should be none — per iteration.

Testing the copy cost with a short string. The small-string optimization reports 0 either way, and the conclusion drawn is the wrong one.

Assuming names match members by name. Matching is positional, exactly as in Chapter 9.8 §3.

Trying to skip a member with _. _ is just a variable name.

Trying to decompose a type with private members. error: cannot decompose private member 'a' of 'Hidden'.

Using it on C++14 or earlier. Measured: a -Wc++17-extensions warning here, and possibly an error elsewhere.

Declaring one without an initializer. Not allowed, same as a reference.

11. Interview Relevance

"What are structured bindings?" A C++17 declaration that names an object's members instead of the object, matching positionally. Mentioning that it works on aggregates, arrays and std::pair/std::tuple is the complete answer.

"Is auto [a, b] = x; a copy?" Yes, and this is the question that is actually being asked. The strong answer gives the three forms and what each means, and ideally notes that writing through a copy fails silently.

"Why const auto& [k, v] in a loop over a map?" To avoid copying each element — and for a map whose values are strings or vectors, that copy allocates. The measurement in §6 is the kind of concrete detail that distinguishes a memorised answer.

"How is this different from std::tie?" std::tie needs the variables declared first, so it cannot deduce types and cannot bind a plain struct. Structured bindings declare and deduce in one step.

12. What You Should Be Able to Say Now

  1. A structured binding declares names for an object's members instead of a name for the object — C++17.
  2. Names are matched positionally, in declaration order. There is no matching by name.
  3. It works on accessible-member aggregates, arrays of known length, and std::pair/std::tuple.
  4. Measured: writing through a copy left r.cycle at 1; writing through auto& set it to 99.
  5. Measured with an allocation counter: auto → 1 allocation, const auto& → 0, auto& → 0.
  6. That measurement needed a string past the inline capacity — a short one reports 0 for every form.
  7. const auto& by default, auto& to modify, plain auto only deliberately — the same rule as Chapter 5.6 and Chapter 6.5.
  8. Plain auto is right when binding a function's return value, because there is nothing to copy twice.
  9. The wrong member count is a compile error that names both numbers — useful when a struct gains a member.
  10. Private members cannot be decomposed, which is the correct refusal.
  11. You cannot ignore a member; _ is an ordinary name. The unused warning covers '[a, b]' as a whole, so naming one you never read is free — and per-member unused detection is lost.
  12. It is a declaration, so Module 7's scope rules apply, it must be initialized, and it works in an if initializer.
  13. Measured: at -std=c++14 it warns decomposition declarations are a C++17 extension.

13. Exercises

Exercise 1 — three forms. For Reading r{1, 2.0};, write the binding that copies, the one that allows modifying r, and the one that is read-only without copying. Say which you would write by default.

Exercise 2 — predict. Given auto [c, v] = r; c = 7; what is r.cycle? Now with auto& [c, v]. Give the reason, not just the values.

Exercise 3 — the loop. Rewrite for (const auto& p : widths) use(p.first, p.second); as a structured binding, and say what const auto& is buying for a std::map<std::string, int>.

Exercise 4 — predict the diagnostic. With struct Reading { int cycle; double value; }; say which compile and give the message otherwise: (a) auto [a] = r; (b) auto [a,b,c] = r; (c) auto [a,b]; (d) const auto [a,b] = r; a = 1; (e) auto [a,_] = r;

Exercise 5 — design the measurement. You suspect a colleague's loop copies each element. Describe how you would prove it, and state the one property your test data must have for the result to mean anything.

Exercise 6 — when the copy is right. Give a case where plain auto [a, b] = f(); is the correct choice and explain why const auto& would buy nothing.

Exercise 7 — the added member. Reading gains a third member. Say what happens to every existing auto [a, b] = ... of it, and argue whether that is a cost or a benefit.

14. Summary

  • A structured binding names an object's members rather than the object — C++17, matched positionally.
  • It works on aggregates, arrays, and std::pair/std::tuple.
  • The auto qualifies the hidden object, so auto copies and auto& does not.
  • Measured: a write through a copy left the original at 1; through a reference it became 99 — silently, with no diagnostic.
  • Measured: auto cost 1 allocation, both reference forms 0 — and only with a string past the inline capacity.
  • const auto& by default — the same rule as Chapters 5.6 and 6.5, and the language's shortest form is the expensive one.
  • Plain auto is right for a function's return value.
  • Count mismatches are compile errors; private members cannot be decomposed; no member can be skipped, though an unused name is free.
  • It is a declaration: scope rules apply, initialization is required, and it combines with the if initializer.
  • Measured: -std=c++14 warns decomposition declarations are a C++17 extension.

This module has now built ten chapters' worth of tools for representing data that is more than one value, and they overlap deliberately. A fixed-size table of numbers can be a built-in array or it can wait for std::vector. A piece of text can be a char array, a std::string, or a std::string_view, and all three appeared with measurements arguing for different ones. A register description can be three loose variables, a struct, or a struct with an enum class member. Every one of those chapters answered how does this work; none of them answered which should I use for the thing in front of me. The last chapter of the module is that question, and it is a judgement chapter in the sense Chapter 6.9 established — no new syntax, a small number of questions to ask about the data itself, and the module's own measurements as the evidence for each answer.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.