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;
};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