Skip to content
VLSI Mentor

C++ Part I · Module 21 · Modern C++ Type and Utility Tools

std::pair and std::tuple

A function that needs to return two things has four options: an output parameter, a small struct, a pair, or — since Chapter 21.5 — an optional wrapping one of those. This chapter is about the middle two and about the boundary between them, which is the part the frozen purpose asks for and the part most treatments skip. The library tools are genuinely useful: std::pair is what the standard library itself returns from map insertion and from minmax_element, std::tuple extends it to any number of values, and std::tie turns a group of values into the lexicographic comparison key that Chapter 20.6 recommended for multi-key sorting. They also cost nothing in space, which the chapter measures rather than assumes — a three-member tuple and a three-member struct came out at exactly twelve bytes each. But the measurement that decides the chapter is a different one, because it is the only place the two genuinely differ, and it is not about size or speed. A plain struct of scalars is trivially copyable, which means it can be memcpy'd, written to a file bytewise, or handed to a hardware interface as raw memory. libc++'s tuple and pair are not, because they have user-provided copy operations — so the thing that looks like a free convenience quietly gives up a property that hardware-facing code depends on.

Intermediate18 min readC++Part IFundamentalsModern C++

Part I · Module 21 · Chapter 21.4 · std::pair and std::tuple

1. What They Are

std::pair<A, B> holds exactly two values, named first and second. std::tuple<Ts...> holds any number, named by position only. Both are class templates with no behaviour of their own beyond construction, copying, comparison and element access.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
std::pair<std::uint32_t, int> p{0x1000, 31};
std::tuple<std::uint32_t, int, bool> t{0x1000, 31, false};

std::tuple is Chapter 18.9's variadic template at work — a parameter pack of types, stored. std::pair predates it and remains separate because first and second are genuinely more readable than get<0> and get<1> when there are only two.

The reason to reach for either is that you want to group values without introducing a type. That saving is real and it is also the chapter's whole tension, because a type is exactly what makes the values readable later.

2. They Cost What a Struct Costs

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof(std::pair<int,double>)              = 16
sizeof(struct { int a; double b; })        = 16  (identical)
sizeof(std::tuple<uint32_t,int,bool>)      = 12
sizeof(NamedResult) (same three members)   = 12  (identical)
sizeof(std::tuple<>)                       =  1  (empty: still 1)

Identical in both cases. The pair<int,double> is 16 rather than 12 for the same reason a struct would be — Chapter 9.8 §5's padding rule, with the double forcing 8-byte alignment. The empty tuple is 1 byte, the empty-class minimum Chapter 20.8 §2 measured for a stateless lambda.

So there is no space argument in either direction. A tuple is not a wrapper around a struct; it is the struct, with the members generated.

3. The One Real Difference

Size is identical. This is not:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
is_trivially_copyable<std::tuple<int,int>> = 0
is_trivially_copyable<std::pair<int,int>>  = 0
is_trivially_copyable<struct{int;double;}> = 1

A plain struct of scalars is trivially copyable; libc++'s tuple and pair are not. The reason is that they declare their own copy and move operations rather than letting the compiler generate them, which is enough to disqualify the type even though the operations do nothing surprising.

That matters in exactly the places this curriculum is aimed at. A trivially copyable type may be:

  • copied with memcpy, which is what Chapter 19.2 §2 established for std::array;
  • written to a file or a socket as raw bytes;
  • handed to a C interface or a DMA descriptor as memory;
  • placed in a buffer whose layout a hardware block reads.

A std::tuple cannot be any of those safely. The compiler will not stop you from memcpy-ing one — it is not a diagnosable error — so this is a property you have to check rather than one you will be told about, and static_assert(std::is_trivially_copyable_v<T>) is the check.

This is the measurement that turns "tuple or struct?" from a style question into a technical one for a whole class of code. It is also a libc++ property rather than a standard guarantee: the standard does not require tuple to be trivially copyable, and does not forbid it either, so an implementation could differ. Check yours.

4. Access

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
get<0>(t) = 0x1000   get<1>(t) = 31   get<2>(t) = 0
get<int>(t) = 31   (by TYPE -- only when the type is unique)
tuple_size = 3

std::get<N>(t) takes a compile-time index. std::get<T>(t) takes a type instead, which is more readable but only legal when that type appears exactly once in the tuple.

The index being compile-time is worth demonstrating, because people reach for a loop over a tuple and are surprised:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
auto t = std::make_tuple(1, 2.5, 'c');
int n = 1;
return (int)std::get<n>(t);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tupidx.cpp:2:76: error: no matching function for call to 'get'

One error. std::get<N> is a template parameter, not an argument, and the whole point is that each element may have a different type — so there is no single return type a run-time index could have. Iterating a tuple requires std::apply (§8) or a compile-time index sequence, which is Chapter 18.9's parameter-pack machinery.

std::tuple_size_v<T> and std::tuple_element_t<N, T> are the traits for asking about the shape in generic code.

5. Structured Bindings Removed the Readability Gap

Before C++17 the argument for a struct included "you can name the members at the point of use". C++17 gave tuples that too:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
auto [addr, latency, error] = asTuple();
auto [a2, l2, e2]           = asStruct();
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
auto [addr, latency, error] = asTuple();
  addr=0x1000 latency=31 error=0
the SAME syntax works on a struct: 0x1000 31 0

Identical syntax for both. That is the important half: structured bindings did not make tuples better than structs, they made the convenience argument disappear from both sides. A struct loses nothing by being a struct.

Two practical notes. auto [a, b] = f(); copies; auto& [a, b] = f(); binds references to the original, and const auto& is the non-copying read. And the names are bindings rather than variables — you cannot leave one out, which is what std::tie with std::ignore is for (§7).

6. Where pair Is Unavoidable

You do not get a choice when the library hands you one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
map::insert -> pair<iterator,bool>: inserted=1 value=100
minmax_element -> pair of iterators: 1 .. 5

std::map::insert returns pair<iterator, bool> — Chapter 19.5 §5 measured the bool reporting whether insertion happened. std::minmax_element returns a pair of iterators — Chapter 20.5 §8 measured it doing both extremes in one pass. equal_range returns a pair of bounds. std::from_chars returns a struct, which is the modern library's own answer to this chapter's question.

So reading pairs is not optional, and structured bindings are how to do it readably:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
auto [it, inserted] = m.insert({1, 100});
if (!inserted) { /* key was already present */ }

That is considerably better than result.second, and it is the single most valuable use of the feature.

7. tie, and the Comparison Key

std::tie builds a tuple of references, which gives it two distinct uses.

Unpacking into variables that already exist:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
std::tie(addr,lat,err) = asTuple() -> 0x1000 31 0
std::ignore skips a field: a=0x2000 (l untouched: 0)

Structured bindings always declare new names; std::tie assigns to existing ones, and std::ignore discards a field you do not want.

And building a lexicographic comparison key, which is the use Chapter 20.6 §6 recommended:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
bool less = std::tie(p.addr, p.latency) < std::tie(q.addr, q.latency);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tie(p.addr,p.latency) < tie(q.addr,q.latency) = 0  (50 < 20 is false)

operator< on a tuple compares the first elements and only moves to the second on a tie, which is exactly a multi-key ordering — and it is guaranteed to be a valid strict weak ordering if each member's < is, which the hand-written a.x < b.x || a.y < b.y form is not. That is the whole reason Chapter 20.6 §6 prefers it, and it is a case where the tuple is the right tool and no struct is wanted.

In C++20 a defaulted operator<=> on the struct generates the same comparison, which is better still when the member order is the sort order.

8. tuple_cat and apply

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tuple_cat -> size 3, get<2> = "x"
std::apply(f, tuple) -> 4.5  (unpacks the tuple into the call)

std::tuple_cat concatenates tuples, which is mostly a library-implementation tool. std::apply(f, t) calls f with the tuple's elements as separate arguments — the inverse of packing them — and it is the clean answer to "how do I iterate a tuple", since the call sees them as a parameter pack.

std::make_from_tuple<T>(t) constructs a T from the elements. Both are Chapter 18.9 §6's perfect forwarding applied to a stored pack, and both are far more common inside libraries than in application code.

9. Where the Struct Wins

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tuple:  std::get<1>(t) = 31     <- what is field 1?
struct: s.latency      = 31     <- self-documenting
sizeof is identical (12 vs 12) so this is purely about reading.

Four arguments, in increasing order of how much they matter.

get<1> says nothing. A reader has to find the declaration to learn what field 1 is. s.latency is the answer at the point of use.

Adding a field renumbers everything after it. Insert a member into the middle of a tuple and every get<N> beyond it silently refers to something else. It compiles if the types happen to line up. Adding a member to a struct breaks nothing.

A struct can have invariants and methods. bool isSlow() const { return latency > 60; } lives with the data. A tuple can carry only values.

And §3's measurement. Trivially copyable, so it can cross a hardware or file boundary as bytes.

Against that, the struct costs one declaration. That is the entire price.

10. The Rule

Use pair or tuple when the grouping is local and short-lived:

  • a function returning two values consumed immediately by the caller;
  • reading what the standard library returned (§6 — not a choice);
  • a comparison key via std::tie (§7);
  • a parameter pack bundled for forwarding inside a template.

Use a struct when the grouping has a name worth having:

  • anything that crosses an interface a different person will read;
  • anything stored in a container or a member;
  • anything with more than two or three fields;
  • anything that needs to be trivially copyable (§3);
  • anything with an invariant, a method, or a meaningful default.

The heuristic that captures most of it: if you are writing get<3>, you wanted a struct. And if the same tuple type appears in two function signatures, you wanted a struct the first time.

11. The Comparison

std::pairstd::tuplea named struct
number of valuesexactly 2anyany
member namesfirst, secondnone — position onlyyours
sizeofsame as a struct (measured 16)same as a struct (measured 12)(measured 16 / 12)
trivially copyable0 (measured)0 (measured)1 (measured)
memcpy / bytewise I/Ononoyes
structured bindingsyesyes (measured)yes (measured, same syntax)
runtime element indexno (measured: 1 error)no (measured)n/a
add a field safely—no — renumbers get<N>yes
can hold invariants / methodsnonoyes
lexicographic < for freeyesyes (measured)C++20 <=>
returned by the libraryyes — map::insert, minmax_elementrarelyincreasingly (from_chars)

12. What to Actually Do

  • Use structured bindings to read anything the library returns — auto [it, inserted] = m.insert(...).
  • Use std::tie for a multi-key comparator. Chapter 20.6 §6's measurement is the reason.
  • Use std::tie with std::ignore when you want some fields into existing variables.
  • Write the struct when the group crosses an interface, is stored, exceeds three fields, or needs an invariant.
  • Write the struct when the bytes matter. Measured: tuple and pair are not trivially copyable.
  • static_assert(std::is_trivially_copyable_v<T>) on any type you intend to memcpy or write out bytewise — nothing else will tell you.
  • Never insert a field into the middle of a tuple that is used anywhere else.
  • Prefer const auto& in a structured binding when you only read, to avoid the copy.
  • Reach for std::apply rather than trying to index a tuple at run time.
  • In C++20, default operator<=> on the struct instead of writing a tie-based comparator.

13. Mistakes Worth Making Once

memcpy-ing a std::tuple. Measured not trivially copyable, and no diagnostic.

Indexing a tuple with a run-time value. One error; the index is a template parameter.

Inserting a field into the middle of a tuple. Every later get<N> now means something else, and it may still compile.

result.second three screens from result.first. The reason structured bindings exist.

auto [a, b] = f(); when f returns a reference to something you meant to modify. It copies; use auto&.

A tuple in two different function signatures. You wanted a struct the first time.

get<T> when the type appears twice. Ambiguous, and the error is longer than the fix.

Reaching for a tuple because "a struct is boilerplate". One declaration, measured identical in size, and it buys names, extensibility and trivial copyability.

14. Interview Relevance

The frozen curriculum marks this chapter High, and the interesting version of the question is the boundary rather than the API.

"When would you use a tuple instead of a struct?" A local, short-lived grouping — a two-value return consumed immediately, a tie comparison key, reading what the library returned. The strong answer then gives the boundary: anything named, stored, crossing an interface, or needing to be trivially copyable gets a struct.

"Is a tuple bigger than a struct?" No — measured identical, same padding rules. Volunteering that the real difference is trivial copyability is what distinguishes the answer.

"What does map::insert return?" pair<iterator, bool>, with the bool saying whether it inserted — and insert never overwrites, which is Chapter 19.5 §5.

"Can you index a tuple at run time?" No. The index is a template parameter because each element may have a different type; std::apply is the way to visit them all.

"Why is std::tie recommended for multi-key comparators?" Tuple operator< is lexicographic and is a valid strict weak ordering by construction, where the hand-written || form is not transitive.

"What did structured bindings change?" They removed the readability advantage the struct used to have at the point of use — and they work on both, so the convenience argument disappeared from the comparison entirely.

15. What You Should Be Able to Say Now

  • That a tuple and the equivalent struct are the same size — measured 12 and 12, 16 and 16 — with the same padding rules.
  • That std::tuple<> is 1 byte, the empty-class minimum.
  • That the one real difference is trivial copyability: 0 for tuple and pair, 1 for the struct — so only the struct can cross a bytewise boundary, and nothing diagnoses the mistake.
  • That it is a libc++ property, not a standard guarantee, so static_assert rather than assume.
  • That structured bindings work identically on both, which removed the convenience argument.
  • That a tuple's index is a compile-time template parameter — measured, a run-time index is one error.
  • That std::tie builds a tuple of references, for unpacking into existing variables and for lexicographic comparison keys.
  • That inserting a field into a tuple silently renumbers every later get<N>.
  • The rule: local and short-lived gets a tuple; named, stored, or bytewise gets a struct.

16. Exercises

Exercise 1 — the sizes. Reproduce §2 for five different member combinations, including one with a std::string. Confirm each tuple matches its struct and explain any padding.

Exercise 2 — trivial copyability. Reproduce §3. Then memcpy a tuple into a byte buffer and back, confirm it appears to work, and write the two sentences explaining why that is not evidence.

Exercise 3 — the renumbering trap. Build a tuple of three values used in four places, insert a fourth field in the middle, and find which call sites still compile and are now wrong.

Exercise 4 — bindings. Read map::insert, minmax_element and equal_range results with structured bindings. Then do it with .first/.second and compare.

Exercise 5 — tie. Write a three-key comparator with std::tie and the same one by hand with ||. Find three elements for which the hand-written one is not transitive.

Exercise 6 — apply. Use std::apply to print every element of a heterogeneous tuple. Then do it with an index sequence and compare the two.

Exercise 7 — the boundary. Take three tuple returns from your own code and decide, with a written reason each, whether they should stay tuples.

17. Summary

  • pair holds two values, tuple any number, named by position only.
  • Measured: identical size to the equivalent struct — 16 and 16, 12 and 12 — with the same padding rules. There is no space argument.
  • Measured: std::tuple<> is 1 byte.
  • Measured, and this is the only real difference: is_trivially_copyable is 0 for tuple and pair and 1 for the struct — so only the struct may be memcpy'd, written out bytewise, or handed to a hardware interface, and nothing diagnoses the mistake. It is a libc++ property, so static_assert rather than assume.
  • Measured: structured bindings work identically on both, which removed the convenience argument from the comparison.
  • Measured: a tuple's index is compile-time — a run-time index is one error, because the elements have different types.
  • std::tie builds a tuple of references — for unpacking into existing variables, and for the lexicographic comparison key Chapter 20.6 §6 recommends.
  • Inserting a field into a tuple renumbers every later get<N> and may still compile.
  • The rule: local, short-lived, or library-imposed gets a tuple; named, stored, interface-crossing, invariant-bearing or bytewise gets a struct. If you are writing get<3>, you wanted a struct.

Carrying several values at once is one shape of "a type that says more about itself". The next is the opposite: representing the absence of a value. Chapter 21.5 is std::optional, and the case for it is made by measuring what the alternatives cost — including a sentinel value that collides with real data, which the chapter demonstrates with a register that legitimately holds the exact value the sentinel had claimed for itself.

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.