Standard Library Utilities
A handful of small, unglamorous standard-library types -- pair, tuple, bitset, numeric_limits, function -- do the unglamorous connective work of gluing everything else together, and knowing them well saves you from reinventing all of them badly.
Not every useful standard library type is a container or an algorithm. A whole category of utility types exists purely to solve small, recurring plumbing problems: bundling two or three unrelated values together without declaring a one-off struct, storing a value that might legitimately be type-erased, packing bits tightly, asking the compiler what a numeric type's actual limits are on this platform, or storing 'any callable thing' regardless of whether it's a function pointer, a lambda, or a bound member function. This page tours std::pair, std::tuple, a short preview of std::optional/std::variant/std::any/std::expected (each gets its own full treatment on the next page), std::bitset, std::numeric_limits, std::function, and std::reference_wrapper -- the everyday utility layer beneath the containers and algorithms you already know.
Why These Utilities Matter
It's tempting to think you only need containers (vector, map, string) and algorithms (sort, find, transform) to write real C++. In practice, a surprising amount of everyday code is about smaller connective problems: a function that needs to return two values instead of one, a value that's genuinely optional, a bit flag set that needs to be compact, or a callback parameter that could be any of several different callable shapes. The standard library has a purpose-built, well-tested answer for each of these instead of leaving you to invent your own struct or workaround every time.
std::pair
std::pair<T1, T2> (from <utility>) bundles exactly two values, of possibly different types, into one object with members .first and .second. It's most familiar as the value type produced by std::map's iterators (a key paired with its value) and as the return type of functions like std::map::insert, which returns a pair of (iterator, bool) telling you where the element landed and whether it was newly inserted.
ages.insert() returns a std::pair -- result.first is an iterator to the (possibly newly-created) element, and result.second is true only if the insertion actually happened. Since "Alice" already exists on the second insert attempt, inserted comes back false and the existing value is left unchanged. auto [it, inserted] = ...; is a C++17 structured binding, which unpacks a pair's two members into named local variables in one line, far more readable than result.first / result.second.
std::pair's .first / .second names carry no meaning of their own -- in a public API, a std::pair<double, double> could be (latitude, longitude) or (min, max) or anything else, and callers have to know which from documentation alone. For anything beyond a quick, obviously-two-things internal helper, a small named struct (struct Range { double min; double max; };) documents itself far better than a std::pair does.
#include <iostream>
#include <map>
#include <utility>
int main()
{
std::map<std::string, int> ages;
std::pair<std::map<std::string, int>::iterator, bool> result =
ages.insert({"Alice", 30});
std::cout << "Inserted: " << std::boolalpha << result.second << "\n";
std::cout << result.first->first << " is " << result.first->second << "\n";
// structured bindings (C++17) read a pair far more naturally:
auto [it, inserted] = ages.insert({"Alice", 31}); // key already exists
std::cout << "Inserted again: " << inserted << "\n";
return 0;
}std::tuple
std::tuple<T1, T2, ..., Tn> (from <tuple>) generalizes std::pair to any fixed number of elements, of any types. Elements are accessed with std::get<index>(t) rather than named members, since there's no fixed .first/.second/.third to generalize to arbitrary length -- but structured bindings work here too and are almost always more readable in practice.
lookupEmployee returns three unrelated values bundled into one tuple rather than needing a dedicated Employee struct just for this one function. std::get<0>/<1>/<2> pull elements out by fixed, compile-time index (indices are checked at compile time -- std::get<5> on a 3-element tuple simply fails to compile). The structured binding form on the second call reads far more naturally, since name, age, and salary carry meaning that get<0>/get<1>/get<2> don't.
Like std::pair, std::tuple is best reserved for quick, local, or generic-programming uses (template metaprogramming frequently needs a tuple's arity and per-element types). For a public-facing function that returns several related values, a named struct is almost always the more maintainable choice -- it self-documents, and adding a field later doesn't silently shift every std::get<N> index after it.
#include <iostream>
#include <tuple>
#include <string>
std::tuple<std::string, int, double> lookupEmployee(int id)
{
return {"Priya", 34, 95000.0}; // name, age, salary
}
int main()
{
auto record = lookupEmployee(101);
std::cout << std::get<0>(record) << " is " << std::get<1>(record)
<< " years old, earning " << std::get<2>(record) << "\n";
auto [name, age, salary] = lookupEmployee(102); // structured binding -- much clearer
std::cout << name << ", " << age << ", " << salary << "\n";
return 0;
}A Quick Preview: optional, variant, any, and expected
Four related utility types round out the modern type-safety toolbox, each solving a different shape of 'this value might not be a plain, single, always-present T': std::optional<T> holds either a T or nothing at all; std::variant<T1, T2, ...> holds exactly one value from a fixed, known set of possible types at a time; std::any can hold a value of literally any type, decided at runtime, with no fixed set declared in advance; and std::expected<T, E> (added in C++23) holds either a successful T or an error E, making failure an explicit part of a function's return type instead of something communicated separately through an exception or an out-parameter.
Each signature communicates a different shape of uncertainty directly in the type system, at compile time, instead of relying on a sentinel value (like returning -1 for 'not found', which collides with legitimate negative results) or an out-parameter the caller might forget to check.
These four are substantial enough, and used often enough in modern C++ interfaces, that the next topic on this site is devoted entirely to them -- this page just establishes what problem each one solves so the rest of the utility landscape makes sense around them.
std::optional<int> findUserId(const std::string &name); // value, or none
std::variant<int, std::string> parseToken(const std::string&); // one of a known set of types
std::any readAnyConfigValue(const std::string &key); // any type at all, decided at runtime
std::expected<int, std::string> parseInt(const std::string&); // C++23: success value, or an errorstd::bitset
std::bitset<N> (from <bitset>) is a fixed-size sequence of N bits, known at compile time, that supports indexing, bitwise operators (&, |, ^, ~), shifting, and conversion to/from strings and integers. It's a purpose-built alternative to hand-rolling bit-flag logic with raw integers and manual shifting and masking.
std::bitset<8> reserves exactly 8 bits and initializes them from the binary literal 0b00101100. .test(2) checks a single bit without needing manual masking (value & (1 << 2)), .count() returns how many bits are set, and .set()/.reset()/.flip() mutate individual bits by index far more readably than raw bitwise operators would. .to_ulong() converts the whole bitset back to a plain integer when you need to interoperate with code that expects one.
std::bitset's size N is a compile-time template parameter, fixed forever once chosen -- it cannot grow or shrink at runtime the way a std::vector<bool> can. For a runtime-determined number of bits, std::vector<bool> (which is specialized to pack bits tightly, similar to bitset) is the better fit; for a fixed, known-in-advance number of flags, bitset's richer bit-manipulation interface usually wins.
#include <iostream>
#include <bitset>
int main()
{
std::bitset<8> flags(0b00101100);
std::cout << "Bits: " << flags << "\n"; // prints 00101100
std::cout << "Bit 2 set? " << flags.test(2) << "\n"; // true
std::cout << "Count set: " << flags.count() << "\n"; // 3
flags.set(0); // turn bit 0 on
flags.reset(5); // turn bit 5 off
flags.flip(3); // toggle bit 3
std::cout << "After changes: " << flags << "\n";
std::cout << "As unsigned long: " << flags.to_ulong() << "\n";
return 0;
}std::numeric_limits
std::numeric_limits<T> (from <limits>) is a compile-time-queryable class template that reports the properties of a numeric type on the current platform and compiler -- its minimum and maximum representable values, how many decimal digits of precision it has, whether it has a sign, and more. This matters because sizes like int and double are not fixed by the C++ standard the way they are in some other languages -- an int is guaranteed to be at least 16 bits, but is 32 bits on essentially every desktop and server platform today, and could differ on an embedded target.
Rather than hard-coding a magic number like 2147483647 for 'the largest int' -- which is wrong on a platform where int has a different width -- std::numeric_limits<int>::max() asks the compiler for the actual, correct answer on whatever platform the code is being built for. This is exactly the kind of thing generic (template) code needs, since it can't know in advance what numeric type T it will be instantiated with.
std::numeric_limits<double>::min() is a common trap: for floating-point types, min() returns the smallest positive representable value, not the most negative one -- use std::numeric_limits<double>::lowest() for the most negative representable value.
#include <iostream>
#include <limits>
template <typename T>
void describe()
{
std::cout << "min: " << std::numeric_limits<T>::min()
<< ", max: " << std::numeric_limits<T>::max()
<< ", is_signed: " << std::numeric_limits<T>::is_signed << "\n";
}
int main()
{
describe<int>();
describe<unsigned char>();
describe<double>();
return 0;
}std::function
std::function<R(Args...)> (from <functional>) is a general-purpose, type-erased wrapper that can hold any callable thing matching a given signature -- an ordinary function, a lambda (with or without captures), a bound member function, or a class with operator() overloaded -- behind one uniform type. This is what makes it possible to write a function parameter or a member variable of type 'a callable that takes an int and returns a bool' without caring what specific kind of callable the caller passes.
The same predicate variable holds a plain free function on one line and a capturing lambda a few lines later -- std::function erases those differences behind one consistent callable interface, storing whatever's needed internally (including the captured threshold) to make the call work later. This flexibility comes at a small cost: std::function typically involves a heap allocation for anything that doesn't fit in its small-object buffer, and calling through it is usually slower than calling a template parameter directly, due to type erasure preventing the compiler from inlining through it.
When performance matters and the callable's exact type is known at the call site (e.g. inside a template function taking the callable as a template parameter, as most STL algorithms do), a plain template parameter is faster than std::function because the compiler can inline through it directly. Reach for std::function specifically when you need to store a callable as a concrete, non-template member variable or pass it through a boundary (like a virtual function or a plugin interface) where templates aren't an option.
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
bool isEven(int n) { return n % 2 == 0; }
int main()
{
std::function<bool(int)> predicate;
predicate = isEven; // plain function
std::cout << predicate(4) << " " << predicate(5) << "\n";
int threshold = 10;
predicate = [threshold](int n) { return n > threshold; }; // capturing lambda
std::cout << predicate(15) << " " << predicate(5) << "\n";
std::vector<int> nums{1, 2, 3, 4, 5, 6};
auto count = std::count_if(nums.begin(), nums.end(), predicate);
std::cout << "Count above threshold: " << count << "\n";
return 0;
}std::reference_wrapper
References can't be stored in most standard containers directly -- a std::vector<int&> doesn't compile, because references aren't reassignable or default-constructible the way container element types need to be. std::reference_wrapper<T> (from <functional>, usually created via the helper functions std::ref and std::cref) is a small, copyable, reassignable object that behaves like a reference but satisfies the requirements containers need.
std::vector<std::reference_wrapper<int>> stores something that behaves like a container of int& even though a literal std::vector<int&> isn't legal C++. Each element still refers to the original a, b, or c, so modifying through refs modifies the originals directly -- the output confirms all three were changed, not copies of them.
std::ref and std::cref (returning a mutable and a const reference_wrapper respectively) show up constantly with std::thread and std::bind, both of which copy their arguments by default -- wrapping an argument in std::ref(x) is how you tell them 'pass this by reference, not by copy' when the underlying mechanism would otherwise copy.
#include <iostream>
#include <vector>
#include <functional>
void doubleValue(int &n) { n *= 2; }
int main()
{
int a = 1, b = 2, c = 3;
std::vector<std::reference_wrapper<int>> refs{a, b, c};
for (int &r : refs)
r += 100;
std::cout << a << " " << b << " " << c << "\n"; // 101 102 103 -- originals modified
return 0;
}Choosing the Right Utility
None of these types compete with each other -- each solves a distinct, narrow problem, and the skill is recognizing which shape of problem you actually have. Bundling two or three unrelated return values for internal or generic use: pair or tuple (a named struct once the API becomes public-facing or long-lived). A value that's present or absent: optional. A value that's one of several known types: variant. A value whose type isn't known until runtime and could be anything: any. A fixed set of bit flags known at compile time: bitset. The true numeric limits of a type on this platform: numeric_limits. A callable of unknown concrete type stored as a member or passed across a boundary: function. A reference that needs to live inside a container or be passed through code that copies by default: reference_wrapper.
Why is a named struct usually preferable to std::pair or std::tuple for a function's public return type, even though pair and tuple work perfectly well technically?
std::pair's .first/.second and std::tuple's get<0>/get<1>/get<2> carry no semantic meaning of their own -- a caller has to consult documentation to know what each position represents, and it's easy to misread or misuse. A named struct with meaningful field names documents itself directly in the code, and adding a new field later doesn't silently renumber or shift the meaning of existing positions.
What is the difference between std::numeric_limits<T>::min() and std::numeric_limits<T>::lowest() for a floating-point type T?
For floating-point types, min() returns the smallest positive representable value (a value very close to zero), not the most negative value. lowest() returns the actual most-negative representable value. For integer types the two are identical, which is part of why the distinction for floating-point types surprises people.
Why can't you declare a std::vector<int&>, and what standard library type is typically used instead when you need reference-like elements in a container?
Standard containers require their element type to be reassignable and satisfy other requirements (like being default-constructible in some contexts) that plain C++ references don't satisfy -- a reference must be bound at initialization and can never be rebound afterward. std::reference_wrapper<T> is used instead: it's a small, copyable, reassignable object that behaves like a reference to T while meeting the requirements containers actually need.
What performance tradeoff does std::function typically introduce compared to passing a callable as a template parameter?
std::function type-erases the stored callable, which usually requires a heap allocation for callables too large to fit in its internal small-object buffer, and prevents the compiler from inlining through the call the way it could with a template parameter whose concrete type is known at compile time. It's worth that cost when you need to store a callable as a concrete member variable or pass it across a non-template boundary; a template parameter is faster when the callable's type is known at the call site.
Give an example of a value that would naturally use std::bitset rather than std::vector<bool>, and explain why.
A fixed, small, compile-time-known set of flags -- for example, an 8-bit hardware status register with exactly 8 named bits that will never change size -- fits std::bitset<8> naturally, since its size is a compile-time template parameter and it offers a richer bit-manipulation interface (test, set, reset, flip, to_ulong) than vector<bool>. A collection of boolean values whose count isn't known until runtime, or that needs to grow and shrink, is a better fit for std::vector<bool> instead.
Write a function minMax(const std::vector<int>&) that returns both the minimum and maximum values in one call using std::pair, and consume the result in main() using a structured binding.
minMax uses std::minmax_element to find both extremes in a single pass, dereferences the two returned iterators to get their values, and returns them bundled together as a std::pair<int, int>. main() consumes the pair with a structured binding, auto [lo, hi] = ..., which is far more readable than result.first / result.second, printing 'Min: 1, Max: 9'.
What does this program print for each of the three bitwise operations?
It prints '0010', '1110', then '1100'. a & b (AND) keeps only bits set in both (bit position 1 from the right, giving 0010); a | b (OR) keeps bits set in either (giving 1110); a ^ b (XOR) keeps bits set in exactly one of the two (giving 1100) -- std::bitset overloads all three bitwise operators to work position-by-position across the whole fixed-size bit sequence, just like the underlying hardware instructions would.
Write a program that stores three different kinds of callables -- a plain function, a capturing lambda, and a std::function wrapping a class with operator() overloaded -- all as std::function<int(int)>, and calls each one in a loop.
All three callables -- the free function square, the capturing lambda, and the AddN struct's operator() -- are stored uniformly as std::function<int(int)>, erasing their different underlying types behind one common interface. Calling each with 5 prints 25 (5*5), 15 (5*3), and 15 (5+10), demonstrating that std::function doesn't care what kind of callable it's holding as long as the call signature matches.
Write a program using std::reference_wrapper (via std::ref) to pass a large struct into a std::vector by reference rather than by copy, and demonstrate that modifying an element through the vector modifies the original object.
std::ref(temp) and std::ref(pressure) wrap references to the original Sensor objects, and the vector stores those reference_wrappers rather than independent copies. The loop's 's.reading += 1.0;' modifies through the wrapper, which modifies the original objects directly -- the output shows temp at 22.5 and pressure at 102.3, confirming no copies were made.
#include <iostream>
#include <bitset>
int main()
{
std::bitset<4> a("1010");
std::bitset<4> b("0110");
std::cout << (a & b) << "\n";
std::cout << (a | b) << "\n";
std::cout << (a ^ b) << "\n";
}