Skip to content
C++
Library
since C++11
Basic

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

cpp
#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

cpp
#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+)

cpp
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+)

cpp
#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+)

cpp
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

cpp
// 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

cpp
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+)

cpp
// 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.14

Heterogeneous sort

cpp
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.

cpp
// 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.

cpp
#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:

cpp
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.14

C++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.

cpp
#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:

cpp
// 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):

cpp
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:

  • T need not be movable — guaranteed copy elision applies when returning the constructed object.
  • The program is ill-formed if T is 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.

cpp
#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 unchanged

After C++17 — prefer structured bindings for new code:

cpp
// 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 cannot

std::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:

cpp
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:

cpp
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 == 7

C++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:

cpp
// 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++23

pair 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:

cpp
// 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)