std::pair and std::tuple
std::pair and std::tuple store multiple values. Structured bindings, std::tie, std::apply, and composition patterns for heterogeneous data.
std::pair and std::tuplesince C++98 (pair), C++11 (tuple)std::pair groups exactly two values; std::tuple groups any number of heterogeneous values, both enabling structured binding and lexicographic comparison.
Overview
std::pair<A, B> and std::tuple<Ts...> are the standard library's answer to returning multiple values from a function without allocating a struct on each call. Before C++17 structured bindings made these ergonomic, they were cumbersome; today they are indispensable for concise multi-value returns, map keys, and template metaprogramming.
std::pair predates the standards body's formal standardization and lives in <utility>. std::tuple, introduced in C++11, generalizes the concept to any number of elements and lives in <tuple>. Both support element access by index, type-based access (where unique), lexicographic ordering, and—since C++17—structured bindings that make them feel like first-class citizens.
Syntax
std::pair
#include <utility>
// Construction (all C++98+)
std::pair<int, std::string> p1(42, "answer");
std::pair<int, std::string> p2{10, "test"};
// C++17: Class Template Argument Deduction (CTAD)
auto p3 = std::pair{5, "five"}; // deduces std::pair<int, const char*>
// Access
int x = p1.first;
std::string s = p1.second;
// C++17: Structured binding
auto [num, text] = p1;
// Comparison (lexicographic, C++98+)
if (p1 < p2) { } // compares .first, then .second if first equal
if (p1 == p2) { }
// Modification
p1.first = 100;
p1.second = "new";
// Make function (C++98+, type deduction)
auto p4 = std::make_pair(3.14, true);std::tuple
#include <tuple>
// Construction (all C++11+)
std::tuple<int, std::string, double> t1(42, "hello", 3.14);
std::tuple<int, std::string, double> t2{42, "hello", 3.14};
// C++17: CTAD
auto t3 = std::tuple{1, "world", 2.71};
// Element access by index (all C++11+)
int a = std::get<0>(t1);
std::string b = std::get<1>(t1);
// Type-based access (C++11+, requires unique type)
int c = std::get<int>(t1);
std::string d = std::get<std::string>(t1);
// Size (C++11 std::tuple_size, C++17 std::tuple_size_v)
constexpr auto sz = std::tuple_size_v<decltype(t1)>; // 3
// Element type reflection (C++11 _t, C++14 _t suffix)
using T0 = std::tuple_element_t<0, decltype(t1)>; // int
using T1 = std::tuple_element_t<1, decltype(t1)>; // std::string
// C++17: Structured binding
auto [x, y, z] = t1;
// Comparison (lexicographic, all C++11+)
if (t1 < t2) { }
if (t1 == t2) { }std::tie (C++11+)
int x; std::string y; double z;
// Assign tuple to variables
std::tie(x, y, z) = t1;
// Use std::ignore to skip unwanted elements
std::tie(x, std::ignore, z) = t1;
// Pre-C++20 comparison pattern (now use <=>)
struct Record {
std::string name;
int score;
bool operator<(const Record& o) const {
return std::tie(name, score) < std::tie(o.name, o.score);
}
};std::apply (C++17+)
#include <tuple>
auto t = std::tuple{1, 2, 3};
// Unpack tuple as function arguments
int sum = std::apply([](int a, int b, int c) { return a + b + c; }, t);
// Forward arguments
template<typename F, typename... Args>
auto call_with_tuple(F f, std::tuple<Args...> t) {
return std::apply(f, std::move(t));
}
// Useful for dependency injection or lazy dispatch
auto callback = [](int x, int y) { return x + y; };
int result = std::apply(callback, args_tuple);std::tuple_cat (C++11+)
auto t1 = std::tuple{1, 2};
auto t2 = std::tuple{"hello", 3.14};
auto t3 = std::tuple_cat(t1, t2);
// t3 is tuple<int, int, const char*, double>{1, 2, "hello", 3.14}Examples
Return multiple values
// Modern: structured binding makes this ergonomic (C++17+)
auto get_bounds(const std::vector<int>& v) -> std::pair<int, int> {
return {*std::min_element(v.begin(), v.end()),
*std::max_element(v.begin(), v.end())};
}
auto [lo, hi] = get_bounds(data);
// Tuple for 3+ values
auto compute_stats(const std::vector<double>& v)
-> std::tuple<double, double, double> {
return {mean(v), median(v), stddev(v)};
}
auto [m, med, sd] = compute_stats(data);Use as map key
std::map<std::pair<int, int>, std::string> grid;
grid[{0, 0}] = "origin";
grid[{1, 0}] = "east";
// Lexicographic ordering: (0,0) < (1,0) < (1,1) etc.Tuple iteration (C++17+)
// Via std::apply and fold expression
auto t = std::tuple{1, "hello", 3.14};
std::apply([](const auto&... args) {
(std::cout << args << '\n', ...); // fold over pack
}, t);
// Prints: 1, hello, 3.14Heterogeneous sort
std::vector<std::tuple<int, std::string, double>> records{
{1, "alice", 95.5},
{2, "bob", 87.0},
{1, "charlie", 92.3}
};
std::sort(records.begin(), records.end());
// Sorted lexicographically: (1, "alice", 95.5), (1, "charlie", 92.3), (2, "bob", 87.0)Best Practices
Use named structs for API returns. If a pair or tuple becomes part of a public interface, give it a name. This aids documentation, searchability, and refactoring.
// Poor for public API
auto query(int id) -> std::pair<std::string, int>; // unclear
// Good
struct User {
std::string name;
int age;
};
User query(int id);Prefer tuples for internal composition. When combining unrelated return values temporarily (e.g., unpacking config, forwarding arguments), tuples are lightweight and clear.
Use std::apply for generic dispatch. When building variadic wrappers or adapting callback signatures, std::apply eliminates manual tuple unpacking.
Leverage structured bindings (C++17+). auto [a, b] = p; is always preferable to auto a = p.first; auto b = p.second;—it's more concise and less error-prone.
Be wary of type-based get<T>() with duplicates. std::get<int>(t) fails to compile if the tuple contains multiple int elements. Fall back to index-based std::get<N>(t) or use a named struct.
Common Pitfalls
Forgetting CTAD exists (C++17+). Many codebases still use std::make_pair() and std::make_tuple() out of habit. Direct construction with CTAD is cleaner: std::pair{a, b} instead of std::make_pair(a, b).
String literal decay in CTAD. std::pair{1, "hello"} deduces std::pair<int, const char*>, not std::pair<int, std::string>. Be explicit if you need std::string.
Structured binding creates copies (by default). auto [a, b] = p; copies; use auto& [a, b] = p; or auto&& [a, b] = std::move(p); to avoid overhead for large types.
Type-based get<T>() is fragile under refactoring. If you add a second int field, std::get<int>(t) breaks silently at compilation. Index-based access is more resilient.
std::tuple_cat() creates new types. The result type depends on the input types; template deduction can become complex. Prefer explicit type annotation or a named return type.
Nested tuples reduce readability. std::tuple<int, std::tuple<std::string, double>> is harder to reason about than a flat struct with named members.
std::apply in Depth (C++17+)
std::apply(f, t) invokes callable f with the elements of tuple-like t expanded as individual arguments. It is the runtime equivalent of manually writing f(std::get<0>(t), std::get<1>(t), ...) but works for any arity known at compile time.
#include <tuple>
#include <utility> // std::pair
// Plain function
int add(int a, int b) { return a + b; }
// Works with std::pair, std::array, or std::tuple
int r1 = std::apply(add, std::pair{3, 4}); // 7
int r2 = std::apply(add, std::tuple{10, 20}); // 30
// Works with lambdas (template argument deduction is deferred to the lambda)
auto mul = [](auto x, auto y) { return x * y; };
float r3 = std::apply(mul, std::pair{2.0f, 3.5f}); // 7.0f
// Variadic forwarding: store arguments now, call later
template <typename F, typename... Args>
auto deferred(F f, Args&&... args) {
auto packed = std::forward_as_tuple(std::forward<Args>(args)...);
return [f, packed = std::move(packed)]() mutable {
return std::apply(f, std::move(packed));
};
}Printing all tuple elements — a classic std::apply + fold-expression pattern:
template <typename... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
std::apply([](const auto&... args) {
std::size_t n = 0;
((std::cout << args << (++n != sizeof...(Ts) ? ", " : "")), ...);
std::cout << '\n';
}, t);
}
print_tuple(std::tuple{1, "hello", 3.14}); // 1, hello, 3.14C++23 change: std::apply's Tuple parameter is now constrained to the tuple-like concept and the function is conditionally noexcept. The practical effect is that std::pair and std::array are officially modelled as tuple-like — what was previously an implementation-defined extension is now guaranteed by the standard.
std::make_from_tuple (C++17+)
std::make_from_tuple<T>(t) constructs an object of type T by expanding the elements of tuple-like t as constructor arguments. It is the construction counterpart to std::apply.
#include <tuple>
struct Point {
int x, y, z;
Point(int x, int y, int z) : x(x), y(y), z(z) {}
};
auto args = std::make_tuple(1, 2, 3);
Point p = std::make_from_tuple<Point>(std::move(args));
// Equivalent to: Point p{1, 2, 3};Why it matters — factory and registry patterns:
// Generic factory: deserialize a tuple from a config source,
// then construct the target type without knowing its arity.
template <typename T, typename Tuple>
T construct(Tuple&& t) {
return std::make_from_tuple<T>(std::forward<Tuple>(t));
}
auto config = load_config(); // returns std::tuple<std::string, int, bool>
auto widget = construct<Widget>(std::move(config));Works with std::pair and std::array too (C++23 formalised):
struct Line { double x1, y1; };
auto endpoints = std::pair{0.0, 1.0};
Line l = std::make_from_tuple<Line>(endpoints);
// Line{0.0, 1.0}Key rules:
Tneed not be movable — guaranteed copy elision applies when returning the constructed object.- The program is ill-formed if
Tis not constructible from the tuple's element types. - For a 1-element tuple, binding a temporary via a reference member would also be ill-formed (C++23 safeguard via
std::reference_constructs_from_temporary_v).
std::tie and std::ignore — the Pre-C++17 Pattern
Before structured bindings (C++17), std::tie was the idiomatic way to unpack a tuple into existing variables. It creates a std::tuple of lvalue references to its arguments.
#include <tuple>
std::pair<double, int> lookup() { return {4.2, 42}; }
// Pre-C++17 style: declare variables first, then unpack
double val;
int count;
std::tie(val, count) = lookup();
// val == 4.2, count == 42
// std::ignore silently discards an element
std::tie(val, std::ignore) = lookup();
// val updated; count unchangedAfter C++17 — prefer structured bindings for new code:
// C++17+: declare and bind in one step
auto [v, c] = lookup();
// std::ignore has no structured-binding equivalent for existing variables.
// std::tie is still useful when you need to assign into pre-declared variables:
std::tie(val, count) = lookup(); // reassigns; structured binding cannotstd::tie for multi-field comparison (pre-C++20):
Before the spaceship operator (<=>), std::tie gave a zero-boilerplate way to implement operator< and operator== on structs:
struct Record {
std::string name;
int score;
double rating;
bool operator<(const Record& o) const {
return std::tie(name, score, rating)
< std::tie(o.name, o.score, o.rating);
}
bool operator==(const Record& o) const {
return std::tie(name, score, rating)
== std::tie(o.name, o.score, o.rating);
}
};C++20 operator<=> replaces this pattern entirely, but std::tie comparisons are still found widely in pre-C++20 codebases.
std::tie to reassign structured-binding variables:
auto v = std::pair{4.2, 42};
auto [m, n] = v; // m == 4.2, n == 42 (copies)
// Reassign m and n via tie — structured binding cannot retarget
std::tie(m, n) = std::pair{1.1, 7};
// m == 1.1, n == 7C++23 Changes to tuple and pair
C++23 introduced several improvements centred on the tuple-like concept (P2165R4), making std::tuple, std::pair, std::array, and std::ranges::subrange interoperable as a unified family.
tuple-like concept (P2165R4)
A type is tuple-like if it supports std::get, std::tuple_element, and std::tuple_size. C++23 formalises this as an exposition-only concept and uses it to constrain std::apply, std::make_from_tuple, std::tuple_cat, and tuple constructors/assignment operators. The practical consequences:
// C++23: std::tuple is now constructible/assignable from any tuple-like type
std::array<int, 3> arr{1, 2, 3};
std::tuple<int, int, int> t = arr; // OK in C++23, ill-formed before
std::pair<int, int> p{10, 20};
std::tuple<int, int> t2 = p; // was already valid; now formally specified
// Comparison between tuple and tuple-like
std::tuple<int, int> ta{1, 2};
std::array<int, 2> ab{1, 2};
bool eq = (ta == ab); // OK in C++23pair forwarding-constructor default arguments (P1951R1 / P2718R0)
std::pair's forwarding constructor gained default template arguments in C++23, resolving some CTAD edge cases where explicit std::pair{a, b} could fail to deduce in constrained contexts.
noexcept on std::apply (P2517R1)
std::apply became conditionally noexcept in C++23:
// C++23: propagates noexcept from f and std::get
constexpr auto safe_sum = [](int a, int b) noexcept { return a + b; };
static_assert(noexcept(std::apply(safe_sum, std::tuple{1, 2})));std::apply and std::make_from_tuple accept all tuple-like types (P2165R4)
These functions are now formally constrained to tuple-like types, making std::apply(f, my_array) and std::make_from_tuple<T>(my_pair) guaranteed portable behaviour rather than a compiler extension.
See Also
- [[structured-bindings]] — C++17 language feature enabling
auto [a, b] = ... - [[auto]] — type deduction for pair/tuple results
- [[templates]] — metaprogramming with tuple indices and types
std::array— fixed-size homogeneous collection (compare with tuple)std::variant— discriminated union (alternative to overloaded tuples)