The need to return or pass several values of different types comes up often enough that almost every C++ programmer has, at some point, written a one-off struct just to bundle them together. std::tuple is the generic answer to that recurring problem: a fixed-size sequence of objects, each potentially of a different type, packaged into a single value that can be passed, returned, stored, and unpacked.
Originally a boost::tuple library type that was adopted nearly verbatim into C++11, the modern std::tuple is one of the cleanest “vocabulary types” the standard library has — provided you know when to reach for it and when a named struct is the better answer.
Three things to take away:
std::tuple<Ts...>packages a fixed-arity, heterogeneous collection of values into one value with no runtime overhead beyond the storage of its elements.- Read tuples with structured bindings (
auto [a, b, c] = t;) for the common case; reach forstd::get<I>(t)when you need one specific element. - Prefer a named struct when the grouping is a domain concept; prefer a tuple when the grouping is ad-hoc, generic, or internal to one function’s interface.
What std::tuple is
A tuple is a compile-time-fixed-size, heterogeneous container. Each element keeps its own type, indexed by position:
#include <tuple>
#include <string>
std::tuple<int, double, std::string> record{ 42, 3.14, "hello" };
Construction can also use CTAD (Nibble #063) — the template arguments are deduced from the constructor:
std::tuple t{ 42, 3.14, std::string{"hello"} }; // CTAD: tuple<int, double, std::string>
Or std::make_tuple, the pre-CTAD factory function that historically applied decay rules (const char* → const char*, references → values):
auto u = std::make_tuple(42, 3.14, "hello"); // tuple<int, double, const char*>
Internally, the tuple is roughly equivalent to a struct with unnamed members in the order you specified — same memory layout in spirit, no virtual dispatch, no allocation. The cost of constructing a tuple is the cost of constructing its elements, no more.
The motivating example
The boost::tuple precursor’s textbook example is the cleanest illustration. Computing statistics over a sequence requires threading three values — count, sum, sum of squares — through the accumulation:
#include <numeric>
#include <tuple>
#include <vector>
using Stats = std::tuple<std::size_t, double, double>; // count, sum, sumSq
Stats accumulate(Stats s, double x){
auto& [count, sum, sumSq] = s; // structured binding to references
++count;
sum += x;
sumSq += x * x;
return s;
}
Stats compute(const std::vector<double>& data){
return std::accumulate(data.begin(), data.end(),
Stats{0, 0.0, 0.0}, accumulate);
}
Without the tuple, this function would need either three parallel std::accumulate calls (slower, less cache-friendly) or a hand-rolled struct (more code for what is genuinely just a “these three values travel together” relationship). The tuple is the right shape: temporary, generic, internal to one algorithm.
Reading values out
C++ has accumulated four ways to extract values from a tuple, and choosing the right one is the difference between idiomatic and awkward.
Structured bindings (C++17). The modern default for unpacking a tuple is the binding syntax:
auto [count, sum, sumSq] = compute(data);
std::cout << "mean = " << sum / count << '\n';
Each name binds to one element by position. The bindings are copies (by default), references-to-the-tuple-elements (auto&), or const references (const auto&), depending on how you write the declaration. This is the read operation you reach for in nearly all application code.
std::get<I> for a single element. When you don’t need to unpack the whole tuple, std::get returns a reference to one specific element by index:
double total = std::get<1>(stats);
std::get<0>(stats) += 1; // can write through it
The index is a compile-time constant, so out-of-range access is a compile error, not a runtime fault. Compare to std::variant (Nibble #058), where std::get<T> can throw — the tuple’s get cannot fail at runtime.
std::get<T> for typed access (C++14). When the tuple contains exactly one element of a given type, you can extract it by type rather than position:
std::tuple<int, double, std::string> t{ 42, 3.14, "hello" };
auto& s = std::get<std::string>(t); // OK: std::string is unique
// auto& d = std::get<int>(t); // OK
// auto& d = std::get<int, int>(...); // would be ambiguous if duplicates
This is a small ergonomic win when the position would be arbitrary. It compiles only when the requested type appears exactly once in the tuple’s parameter pack.
std::tie for unpacking into existing variables. Before structured bindings, the canonical “unpack into named locals” idiom was std::tie, which builds a tuple of references and assigns through it:
std::size_t count;
double sum, sumSq;
std::tie(count, sum, sumSq) = compute(data);
std::tie is still the right tool when the destination variables already exist (you’re updating them, not declaring new ones), or when you want to ignore part of the tuple with std::ignore:
std::tie(count, std::ignore, sumSq) = compute(data);
The other use of std::tie: lexicographic comparison
std::tie has a second life as the pre-C++20 idiom for multi-key comparison. Tuples define lexicographic ordering on their elements, and tie lets you compare any group of named fields in declaration order:
struct Version {
int major, minor, patch;
bool operator<(const Version& o) const {
return std::tie(major, minor, patch)
< std::tie(o.major, o.minor, o.patch);
}
};
The < on the two tuples runs element-wise: compare major first, then minor if equal, then patch. This is exactly what a hand-written cascade of ifs would do, with none of the boilerplate and none of the chances to typo a field. C++20’s <=> (Nibble #068) supplanted this idiom for newly-written code, but std::tie remains the right tool when you can’t yet adopt <=> or when only some operators need the lexicographic shape.
std::apply: tuple as function-call argument list
std::apply (C++17) takes a callable and a tuple, and invokes the callable with the tuple’s elements as separate arguments:
double computeArea(double w, double h, double depth){
return w * h * depth;
}
std::tuple<double, double, double> dims{ 2.0, 3.0, 4.0 };
double area = std::apply(computeArea, dims); // calls computeArea(2.0, 3.0, 4.0)
This is the bridge between “values bundled in a tuple” and “values passed as separate arguments” — useful when a tuple flows through generic code that eventually has to call a specific function with its elements.
Tuple or struct? A decision rule
The recurring question with tuples is when to use one instead of a named struct. The honest answer is that tuples are right for a specific niche, and struct is right for everything else.
Reach for a tuple when:
- The grouping is internal to one function’s interface and doesn’t need a name elsewhere — return values from helper functions, accumulator state in a fold, intermediate results in pipelines.
- The code is generic and doesn’t know the meaning of the values it’s bundling — utility templates, library primitives, type-list manipulation.
- You only need positional access, and the positions have obvious meanings at the call site (or are immediately unpacked into named bindings).
Prefer a named struct when:
- The grouping has meaning beyond one function. If two separate functions both produce a “user record,” it’s a struct, not a tuple — naming the type documents the concept.
- The fields have different purposes that aren’t obvious from position alone.
std::tuple<double, double, double>could be a 3D point, RGB colour, or width-height-depth;struct Vec3 { double x, y, z; }documents itself. - You’d benefit from member functions, constructors with validation, or the ability to add fields later without breaking call sites.
The shorthand: structs encode domain concepts; tuples encode ad-hoc bundles. If you’d be tempted to write a comment explaining what the elements mean, you wanted a struct.
Takeaway
std::tuple is the standard library’s answer to “I have several values of different types that need to travel together.” Construct it directly or with CTAD; read it with structured bindings for the common case, std::get<I> for single-element access, and std::tie for unpacking into existing variables or for lexicographic comparison.
Reach for std::apply when the tuple needs to become a function-call argument list. The rule for choosing between tuple and struct is simple: if the grouping has a name worth giving it, write a struct; if it’s an ad-hoc bundle inside one function or one template, write a tuple.