Menu

C++ pair and tuple: Group Values Without a Struct

How std::pair and std::tuple bundle two or more values into one object - making them, accessing the fields, structured bindings, and where each one fits.

This page includes runnable editors - edit, run, and see output instantly.

Two Values, One Object

Sometimes you need to keep two things together - a name and a score, an x and a y, a "did it work" flag and the result. You could define a struct for every such pairing, but for throwaway groupings that is a lot of ceremony. std::pair (from <utility>) bundles exactly two values into one object, and std::tuple (from <tuple>) generalizes that to any fixed number of values.

You already met std::pair indirectly on the way here: every element of a std::map is a pair<const Key, Value>. This page makes that explicit and shows the modern, readable ways to build and unpack these types.

The two members are always called .first and .second - they don't take the names of your variables. That's the price of a generic type: the field names are positional, not descriptive.

Making Pairs

There are three common ways to create a pair, and they all produce the same object.

make_pair deduces the element types for you, which was handy before C++17. Today brace init plus class-template argument deduction (pair p{"Boris", 85};) covers most cases, but you'll still see make_pair everywhere in existing code.

One gotcha with deduction: make_pair("hi", 3) deduces pair<const char*, int>, not pair<string, int>. String literals are not std::string. If you need a string, say so explicitly - make_pair(string("hi"), 3) or spell out the pair type - otherwise you may get surprising comparisons or copies later.

Unpacking With Structured Bindings

Reading .first and .second everywhere gets unreadable fast, because the names tell you nothing. C++17 structured bindings let you give the two fields real names in one line:

This shines in a range-based for over a map, where each element is a pair. Instead of it->first / it->second, you name the key and value directly:

Use const auto& in the loop, just as you would for any container element - it avoids copying each pair and signals you're only reading. Drop the & and you copy every entry; that's a quiet performance bug on a large map.

When Two Isn't Enough: tuple

A pair stops at two values. When you need three or more, std::tuple is the same idea with an arbitrary count. You build one with brace init or make_tuple, and you read it with std::get<N>, where N is a compile-time index.

The index inside get<> must be a constant known at compile time. get<i>(record) where i is a runtime variable will not compile - a tuple's fields can have different types, so the element type has to be resolved during compilation, not at run time. If you find yourself wanting a runtime index, you probably want a vector instead.

Structured bindings work on tuples too, and this is the readable way to consume one:

Returning Multiple Values

The everyday reason to reach for these types is returning more than one value from a function without inventing a struct or juggling output parameters. Bundle the results into a pair or tuple and unpack at the call site.

For three or more results, return a tuple the same way. There's also std::tie, an older trick that unpacks into existing variables rather than declaring new ones - useful when you want to ignore a field with std::ignore:

When to stop, though: if the same group of fields shows up in more than one place, or you keep forgetting whether .second is the score or the count, define a struct with named members. pair and tuple are best for local, short-lived groupings; named fields win the moment the data lives longer than a single expression.

Comparing and Sorting

A handy bonus: pair and tuple come with built-in comparison operators that work lexicographically - they compare the first element, and only fall back to the next when the firsts are equal. That makes them perfect keys for sorting.

Notice the order of the fields matters: putting age first sorts by age primarily, then by name as a tiebreaker. If you wanted name-first ordering, you'd swap the tuple's element order. This default comparison is exactly why pair<priority, item> is a common idiom for priority queues.

Next: Iterators

You've now seen .first, .second, it->first, and *it show up around containers - the thing that actually connects a pair element to the map it lives in is an iterator. The next page unpacks iterators properly: what begin() and end() really return, how ++it walks a container, and the iterator-invalidation traps that cause some of the nastiest undefined behavior in C++.

Frequently Asked Questions

What is the difference between pair and tuple in C++?

std::pair holds exactly two values, accessed with .first and .second. std::tuple holds any fixed number of values (zero, two, three, or more), accessed with std::get<N>(t). A pair is essentially a two-element tuple with friendlier member names; reach for tuple only when you need three or more fields.

How do you access tuple elements in C++?

Use std::get<N>(t) with a compile-time index, e.g. std::get<0>(t). From C++17 you can also unpack with structured bindings: auto [a, b, c] = t; gives each element its own named variable. You cannot index a tuple with a runtime variable - std::get<i> requires a constant i.

How do you return multiple values from a function in C++?

Return a std::pair or std::tuple and unpack it at the call site with structured bindings: auto [ok, value] = parse(text);. This is cleaner than output parameters and avoids defining a one-off struct, though a named struct is more readable when the fields outlive a single call.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED