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++11Fixed-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_tupleorapply
Avoid when:
- You have more than ~4–5 related fields (use a named
structinstead for clarity) - You need heterogeneous containers (use
std::variant) - Semantics matter more than convenience (struct communicates intent better)
Syntax
pair
#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_orderingtuple
#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}; // falsetuple utilities
// 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
// 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
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
// 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(); // 42Type-safe parameter unpacking
// 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.
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>().
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:
// 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:
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++11Be mindful of const-correctness with tuples containing references.
A const tuple of references still allows modification through the references.
int x = 42;
const auto t = std::tuple<int&>(x);
std::get<0>(t) = 99; // allowed; t is const, but int& is notCommon Pitfalls
Copies happen by default, not references
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 modifiedType deduction with const char* in tuples
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
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 insteadTuple unpacking with std::tie and rvalue references
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 captureLarge tuples are hard to maintain
Beyond 4–5 fields, tuples become difficult to reason about. Prefer a struct with named members:
// 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;
};std::apply — Calling a Function with Tuple Elements
std::apply unpacks a tuple and passes its elements as individual arguments to a callable. This is the primary bridge between tuple-based argument storage and actual function invocation, enabling patterns like deferred calls, type-safe wrappers, and compile-time dispatch over heterogeneous argument lists.
// C++17
#include <tuple>
#include <functional>
int add(int a, int b, int c) { return a + b + c; }
auto args = std::make_tuple(10, 20, 30);
int result = std::apply(add, args); // 60
// Works with lambdas too
auto product = std::apply(
[](int x, int y) { return x * y; },
std::make_tuple(6, 7)
); // 42std::apply works with any callable: free functions, lambdas, std::function, and member function pointers. When the callable is a member function, pass the object as the first tuple element.
// C++17
struct Calculator {
int scale;
int multiply(int x, int y) const { return scale * x * y; }
};
Calculator calc{2};
// Member function: first element is the object (or pointer)
auto result = std::apply(
&Calculator::multiply,
std::make_tuple(calc, 3, 4) // 2 * 3 * 4 = 24
);std::make_from_tuple — Constructing Objects from Tuples
std::make_from_tuple<T> constructs a T by forwarding a tuple's elements as constructor arguments. This is the natural complement to std::apply when the target is a constructor rather than a free function, enabling factory patterns and perfect-forwarding construction from stored argument packs.
// C++17
#include <tuple>
struct Config {
std::string host;
int port;
bool tls;
Config(std::string h, int p, bool t)
: host{std::move(h)}, port{p}, tls{t} {}
};
auto params = std::make_tuple(std::string("localhost"), 8080, true);
auto cfg = std::make_from_tuple<Config>(params);
// Equivalent to: Config{"localhost", 8080, true}This is especially useful when parameters arrive as a tuple from another layer (e.g., a factory registry, a deserialization step, or a thread-pool task queue) and you need to construct the concrete type at the point of use.
// C++17: Generic factory function
template<typename T, typename... Args>
T construct_from(std::tuple<Args...> arg_pack) {
return std::make_from_tuple<T>(std::move(arg_pack));
}
struct Point { int x, y, z; };
auto p = construct_from<Point>(std::make_tuple(1, 2, 3));
// p.x=1, p.y=2, p.z=3Iterating Tuple Elements with std::index_sequence
Tuples are heterogeneous, so a runtime loop cannot iterate their elements — you need a compile-time index sequence instead. The standard idiom uses std::index_sequence and an immediately-invoked lambda or helper to expand a parameter pack over every index simultaneously.
// C++14
#include <tuple>
#include <utility>
#include <iostream>
// Helper: visit each element with its index
template<typename Tuple, std::size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}
template<typename... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
print_tuple_impl(t, std::index_sequence_for<Ts...>{});
std::cout << '\n';
}
auto t = std::make_tuple(42, 3.14, std::string("hi"));
print_tuple(t); // 42, 3.14, hiC++17 fold expressions make the expansion site cleaner. The key pattern is always the same: a std::index_sequence_for<Ts...> generates the compile-time pack 0, 1, ..., N-1, which drives std::get<Is>(t) expansions.
// C++17: Apply a callable to every element (for_each_in_tuple)
template<typename Tuple, typename F, std::size_t... Is>
void for_each_impl(Tuple&& t, F&& f, std::index_sequence<Is...>) {
(f(std::get<Is>(std::forward<Tuple>(t))), ...); // fold over comma
}
template<typename... Ts, typename F>
void for_each_in_tuple(std::tuple<Ts...>&& t, F&& f) {
for_each_impl(std::move(t), std::forward<F>(f),
std::index_sequence_for<Ts...>{});
}
auto data = std::make_tuple(1, 2.5, std::string("three"));
for_each_in_tuple(std::move(data), [](auto& v) {
std::cout << v << '\n';
});Structured Bindings with Tuples
Structured bindings (C++17) are the cleanest way to unpack a tuple into named local variables. They eliminate std::get<N> noise and make code at the call site read like natural destructuring, similar to Python tuple unpacking or Rust's let (a, b) = ....
// C++17
auto get_stats(const std::vector<int>& v)
-> std::tuple<int, int, double> // min, max, mean
{
auto [lo, hi] = std::minmax_element(v.begin(), v.end());
double mean = std::accumulate(v.begin(), v.end(), 0.0) / v.size();
return {*lo, *hi, mean};
}
auto [min_val, max_val, avg] = get_stats({3, 1, 4, 1, 5, 9, 2, 6});
// min_val=1, max_val=9, avg=3.875Structured bindings bind by value (copy) by default. Use auto& to bind by reference and avoid copies, or auto&& for forwarding references in generic code.
// C++17: Binding to references avoids copies
std::tuple<std::string, std::vector<int>> heavy{"key", {1, 2, 3, 4, 5}};
auto& [key, values] = heavy; // no copy; key and values are references
values.push_back(6); // modifies heavy directly
const auto& [k, v] = heavy; // read-only reference bindingStructured bindings also work with std::pair, arrays, and any type that provides get<N> and tuple_size specializations — making them a general unpacking protocol, not just a tuple feature.
std::tuple_cat — Concatenating Multiple Tuples
std::tuple_cat merges any number of tuples into a single tuple whose type is the concatenation of all element types. The result type is computed entirely at compile time, making this a zero-overhead operation with perfect forwarding of elements.
// C++11
auto prefix = std::make_tuple(1, 2.0);
auto suffix = std::make_tuple(std::string("three"), 'x');
auto combined = std::tuple_cat(prefix, suffix);
// type: tuple<int, double, std::string, char>
std::cout << std::get<2>(combined); // "three"std::tuple_cat accepts rvalue tuples and moves elements rather than copying, so large elements are transferred efficiently.
// C++11: Prepend or append a single element using a one-element tuple
template<typename T, typename... Ts>
auto prepend(T&& val, std::tuple<Ts...> tup) {
return std::tuple_cat(std::make_tuple(std::forward<T>(val)),
std::move(tup));
}
auto original = std::make_tuple(2, 3, 4);
auto extended = prepend(1, std::move(original));
// tuple<int, int, int, int> holding {1, 2, 3, 4}A practical use is building a type list or argument pack incrementally across template specialisations, where each layer appends its own type to a growing tuple before the final std::apply call.
Forwarding Tuples in Variadic Templates
std::forward_as_tuple creates a tuple of forwarding references — the standard idiom for capturing a variadic argument pack and passing it elsewhere without copies. Unlike std::make_tuple, which copies or moves every argument, forward_as_tuple stores references, making it suitable for deferred invocation where the arguments outlive the tuple.
// C++11
template<typename F, typename... Args>
auto defer_call(F&& f, Args&&... args) {
// Capture by forwarding reference — no copies made here
return [f = std::forward<F>(f),
args_tup = std::forward_as_tuple(std::forward<Args>(args)...)]()
mutable {
return std::apply(std::move(f), std::move(args_tup));
};
}
auto task = defer_call([](int a, int b) { return a + b; }, 10, 32);
int answer = task(); // 42The critical rule: forward_as_tuple tuples hold references, so the referenced objects must remain alive for the duration of the tuple's use. If the tuple outlives the call site, store actual values via make_tuple instead.
// C++17: Combining forward_as_tuple with apply for perfect-forwarding dispatch
template<typename Callable, typename... Args>
decltype(auto) invoke_logged(Callable&& fn, Args&&... args) {
auto arg_pack = std::forward_as_tuple(std::forward<Args>(args)...);
// Log argument count at compile time
constexpr std::size_t n = std::tuple_size_v<decltype(arg_pack)>;
std::cout << "calling with " << n << " argument(s)\n";
return std::apply(std::forward<Callable>(fn), std::move(arg_pack));
}
invoke_logged(std::plus<int>{}, 3, 4); // prints "calling with 2 argument(s)", returns 7std::forward_as_tuple is also the engine behind std::piecewise_construct for std::map::emplace and std::pair's piecewise constructor, where constructor arguments for each half of a pair are passed as separate forwarding tuples.
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 argumentsstd::reference_wrapper(C++11) — create reference-holding tuplesstd::array(C++11) — homogeneous fixed-size sequences- Template Parameter Packs (C++11) — variadic templates and forwarding patterns