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

std::tuple and std::pair

Fixed-size heterogeneous collections with value semantics — pair for two values, tuple for N values of any types, with structured bindings support.

std::tuple and std::pairsince C++11

Fixed-size, heterogeneous collections with compile-time type information and value semantics — std::pair<A, B> for two elements, std::tuple<Ts...> for any number of elements of different types.

Overview

std::pair (C++98, modernized in C++11) and std::tuple (C++11) are the standard library's primary tools for grouping values of different types. They are heterogeneous: each element can have a distinct type. They have value semantics: they are copyable, movable, and comparable by default.

Both are template types whose structure is known at compile time, not runtime. This is their strength: you get zero-cost abstractions, type safety, and structured unpacking (C++17+) without runtime overhead.

Use when:

  • Returning multiple values from functions
  • Creating composite keys for associative containers
  • Building blocks for higher-level abstractions (with templates or apply)
  • Forwarding argument packs with forward_as_tuple or apply

Avoid when:

  • You have more than ~4–5 related fields (use a named struct instead for clarity)
  • You need heterogeneous containers (use std::variant)
  • Semantics matter more than convenience (struct communicates intent better)

Syntax

pair

cpp
#include <utility>

// C++11: aggregate initialization
std::pair<int, std::string> p = {42, "hello"};
p.first;   // int, value 42
p.second;  // std::string, value "hello"

// Explicit construction (C++11)
std::pair<int, std::string> p2(3.14, "world");  // narrows 3.14 to int

// C++11: make_pair deduces types (deprecated in C++17 in favor of direct init)
auto p3 = std::make_pair(1, 'a');  // pair<int, char>

// C++17: structured bindings for unpacking
auto [num, str] = p;  // num = 42, str = "hello"

// Comparison (lexicographic, C++11)
std::pair{1, "a"} < std::pair{1, "b"};  // true

// C++20: three-way comparison
std::pair{1, "a"} <=> std::pair{2, "a"};  // std::strong_ordering

tuple

cpp
#include <tuple>

// C++11: aggregate initialization
std::tuple<int, double, std::string> t = {1, 2.5, "hi"};

// C++11: Access by index (compile-time constant)
std::get<0>(t);  // 1
std::get<1>(t);  // 2.5
std::get<2>(t);  // "hi"

// C++11: Access by type (if type is unique in the tuple)
std::get<std::string>(t);  // "hi"

// C++17: Structured bindings (most readable for small tuples)
auto [n, d, s] = t;

// C++11: make_tuple with type deduction
auto t2 = std::make_tuple(42, 3.14, "test");  // tuple<int, double, const char*>

// C++11: Compile-time size and type info
constexpr size_t size = std::tuple_size_v<decltype(t)>;  // 3
using elem_type = std::tuple_element_t<1, decltype(t)>;  // double

// C++11: Compare lexicographically
std::tuple{1, 2, 3} < std::tuple{1, 2, 4};  // true
std::tuple{1, 3, 0} < std::tuple{1, 2, 4};  // false

tuple utilities

cpp
// C++11: tie — creates a tuple of references for unpacking
int a; double b; std::string c;
std::tie(a, b, c) = t;        // unpacks t into existing variables
std::tie(a, std::ignore, c) = t;  // skip the double

// C++11: forward_as_tuple — creates a tuple of forwarding references
template<typename... Args>
void store(Args&&... args) {
    auto fwd = std::forward_as_tuple(std::forward<Args>(args)...);
    // fwd holds references; perfect forwarding, no copies
}

// C++11: tuple_cat — concatenate multiple tuples
auto t1 = std::make_tuple(1, 2);
auto t2 = std::make_tuple("a", 3.14);
auto combined = std::tuple_cat(t1, t2);  // tuple<int, int, const char*, double>

// C++17: apply — call a function with tuple elements as arguments
auto add = [](int a, int b) { return a + b; };
auto args = std::make_tuple(3, 4);
int result = std::apply(add, args);  // 7

// C++20: make_from_tuple — construct a type from a tuple
struct Point { int x, y; };
auto coords = std::make_tuple(10, 20);
auto p = std::make_from_tuple<Point>(coords);  // Point{10, 20}

Examples

Returning multiple values

cpp
// C++11: Return tuple instead of output parameters
std::tuple<bool, std::string, std::vector<int>> parse_data(std::string_view input) {
    // ... validation and parsing
    if (valid) {
        return std::make_tuple(true, "success", results);
    }
    return std::make_tuple(false, "parse error", {});
}

// C++17: Unpack cleanly with structured bindings
auto [ok, msg, data] = parse_data(input);

Composite map keys

cpp
std::map<std::pair<int, int>, std::string> grid;
grid[{0, 0}] = "origin";
grid[{5, 3}] = "point";

// Tuples work similarly for multi-dimensional indexing
std::map<std::tuple<int, int, int>, std::string> space;
space[{1, 2, 3}] = "coordinate";

Forwarding and deferred execution

cpp
// C++11: Capture arguments without copying
template<typename F, typename... Args>
auto defer(F f, Args&&... args) {
    auto captured = std::forward_as_tuple(std::forward<Args>(args)...);
    return [f, captured]() { return std::apply(f, captured); };
}

int multiply(int a, int b) { return a * b; }

auto later = defer(multiply, 6, 7);
int result = later();  // 42

Type-safe parameter unpacking

cpp
// C++11 + C++20: Variadic template with fold expressions
template<typename... Args>
void log_all(Args&&... args) {
    auto tup = std::forward_as_tuple(std::forward<Args>(args)...);
    std::apply([](auto&&... vals) {
        (std::cout << ... << vals);  // C++17 fold expression
    }, tup);
}

log_all(1, " + ", 2, " = ", 3);

Best Practices

Prefer direct aggregate initialization over make_tuple.
Since C++17, std::tuple{...} is cleaner than std::make_tuple(...) and deduces types correctly.

cpp
auto t = std::tuple{1, 2.5, "hi"};  // C++17+
// instead of std::make_tuple(1, 2.5, "hi");

Use structured bindings for small tuples (C++17+).
For unpacking 2–3 elements, structured bindings beat .first, .second, or std::get<N>().

cpp
auto [x, y, z] = get_coordinates();  // clearer than std::get<0>(...)

Use a named struct for semantic clarity.
Tuples are convenient for temporary groupings, but named structs communicate intent better:

cpp
// Tuple: unclear at call site
auto result = std::make_tuple(name, age, salary);

// Struct: self-documenting
struct Employee { std::string name; int age; double salary; };
auto result = Employee{name, age, salary};

Use std::reference_wrapper or std::tie for reference semantics.
By default, std::tuple copies elements. To bind to references:

cpp
int x = 42;
auto t = std::tie(x);  // tuple<int&>
std::get<0>(t) = 99;   // x is now 99

// Or explicitly:
auto t2 = std::tuple<int&>(x);  // C++11

Be mindful of const-correctness with tuples containing references.
A const tuple of references still allows modification through the references.

cpp
int x = 42;
const auto t = std::tuple<int&>(x);
std::get<0>(t) = 99;  // allowed; t is const, but int& is not

Common Pitfalls

Copies happen by default, not references

cpp
std::vector<int> data = {1, 2, 3};
auto t = std::make_tuple(data);  // copies the vector!
std::get<0>(t).push_back(4);     // doesn't affect the original

// Use reference_wrapper to capture by reference (C++11)
auto t2 = std::make_tuple(std::ref(data));
std::get<0>(t2).push_back(4);  // now the original is modified

Type deduction with const char* in tuples

cpp
auto t = std::make_tuple(42, "hello");
// t has type tuple<int, const char*>, not tuple<int, string>

// Explicitly create std::string if needed
auto t2 = std::make_tuple(42, std::string("hello"));

Accessing by type fails if the type appears multiple times

cpp
std::tuple<int, int, std::string> t = {1, 2, "hi"};
std::get<std::string>(t);  // ok: "hi"
std::get<int>(t);          // ERROR: ambiguous; two ints in the tuple
std::get<0>(t);            // ok: use index instead

Tuple unpacking with std::tie and rvalue references

cpp
auto get_data() {
    return std::make_tuple(42, "test");
}

std::string str;
std::tie(std::ignore, str) = get_data();
// str contains a copy; the temporary string is destroyed
// This is safe (copy is fine), but be aware if the original intent was reference capture

Large tuples are hard to maintain

Beyond 4–5 fields, tuples become difficult to reason about. Prefer a struct with named members:

cpp
// Tuple: unclear what each field represents
auto result = std::make_tuple(id, name, dept, salary, start_date, manager_id);

// Struct: self-explanatory
struct EmployeeRecord {
    int id;
    std::string name;
    std::string dept;
    double salary;
    std::chrono::year_month_day start_date;
    int manager_id;
};

See Also

  • std::variant (C++17) — for heterogeneous containers or tagged unions
  • Structured Bindings (C++17) — best-practice unpacking for tuples and pairs
  • std::apply (C++17) — invoke functions with tuple arguments
  • std::reference_wrapper (C++11) — create reference-holding tuples
  • std::array (C++11) — homogeneous fixed-size sequences
  • Template Parameter Packs (C++11) — variadic templates and forwarding patterns