Min/Max Algorithms
Complete guide to std::min, std::max, std::minmax, clamp, and their range-element variants—signatures, version history, pitfalls, and idiomatic C++20 usage.
Min/Max Algorithmssince C++98A family of functions and algorithms in <algorithm> that compute minimum and maximum values of two scalars, initializer lists, or iterator ranges, with optional custom comparators and projections.
Overview
The min/max family spans four generations of the standard:
| Function | Introduced | Operates on |
|---|---|---|
std::min, std::max | C++98 | two values |
| Initializer-list overloads | C++11 | {a, b, c, …} |
std::minmax, std::minmax_element | C++11 | values / ranges |
std::clamp | C++17 | single value against bounds |
std::ranges::min, ranges::max, etc. | C++20 | ranges with projections |
All scalar overloads became constexpr in C++14. The element algorithms (min_element, max_element) became constexpr in C++20.
Syntax
Value comparison
// C++98 — returns const reference to the smaller/larger argument
template<class T>
const T& std::min(const T& a, const T& b);
template<class T, class Compare>
const T& std::min(const T& a, const T& b, Compare comp);
// C++11 — initializer-list overloads return by value (T, not reference)
template<class T>
T std::min(std::initializer_list<T> ilist);
template<class T, class Compare>
T std::min(std::initializer_list<T> ilist, Compare comp);
// std::max mirrors std::min exactlyCombined min and max
// C++11
template<class T>
std::pair<const T&, const T&> std::minmax(const T& a, const T& b);
template<class T>
std::pair<T, T> std::minmax(std::initializer_list<T> ilist);Range element algorithms
// C++98 — require ForwardIterator; return iterator to first min/max
template<class ForwardIt>
ForwardIt std::min_element(ForwardIt first, ForwardIt last);
template<class ForwardIt>
ForwardIt std::max_element(ForwardIt first, ForwardIt last);
// C++11 — returns pair<ForwardIt, ForwardIt>
template<class ForwardIt>
std::pair<ForwardIt, ForwardIt> std::minmax_element(ForwardIt first, ForwardIt last);Clamping (C++17)
template<class T>
constexpr const T& std::clamp(const T& v, const T& lo, const T& hi);
template<class T, class Compare>
constexpr const T& std::clamp(const T& v, const T& lo, const T& hi, Compare comp);C++20 Ranges versions
// std::ranges::min / max / minmax — accept a range directly, plus projection
namespace std::ranges {
template<std::input_range R, class Proj = std::identity>
auto min(R&& r, auto comp = {}, Proj proj = {}); // C++20
template<std::forward_range R, class Proj = std::identity>
auto min_element(R&& r, auto comp = {}, Proj proj = {}); // C++20
}Examples
Basic scalar use
#include <algorithm>
#include <cassert>
int a = 7, b = 3;
int lo = std::min(a, b); // C++98 — returns 3
int hi = std::max(a, b); // C++98 — returns 7
auto [mn, mx] = std::minmax(a, b); // C++11 — structured bindings (C++17)
assert(mn == 3 && mx == 7);
// Three-way comparison with initializer list — C++11
int x = 5, y = 2, z = 8;
int smallest = std::min({x, y, z}); // 2
int largest = std::max({x, y, z}); // 8Type mismatch — a common trap
int n = 10;
long m = 20;
// std::min(n, m); // ERROR — deduction produces two conflicting T
std::min<long>(n, m); // OK — explicit template argument
std::min(n, static_cast<int>(m)); // OK — cast to matching typeRange algorithms
#include <algorithm>
#include <vector>
#include <format> // C++20
#include <iostream>
std::vector<int> scores = {85, 93, 78, 90, 96, 82};
auto min_it = std::min_element(scores.begin(), scores.end()); // C++98
auto max_it = std::max_element(scores.begin(), scores.end()); // C++98
// Always guard against empty range before dereferencing
if (min_it != scores.end())
std::cout << std::format("range: {} – {}\n", *min_it, *max_it); // C++20
// C++11 — single pass for both
auto [lo_it, hi_it] = std::minmax_element(scores.begin(), scores.end()); // C++11Custom comparator
struct Person { std::string name; int age; };
std::vector<Person> team = {{"Alice", 34}, {"Bob", 29}, {"Carol", 41}};
auto youngest = std::min_element(team.begin(), team.end(),
[](const Person& a, const Person& b) { return a.age < b.age; });
// *youngest == {"Bob", 29}C++20 ranges with projection
// Projections eliminate the need for a comparator lambda wrapping member access
auto oldest = std::ranges::max_element(team, {}, &Person::age); // C++20
// *oldest == {"Carol", 41}
// ranges::min on a range directly — no iterator pair needed
int peak = std::ranges::max(scores); // C++20 — returns 96std::clamp (C++17)
#include <algorithm>
// Clamp a sensor reading to a safe operating window
constexpr int kMinRPM = 500, kMaxRPM = 8000;
int raw_rpm = 9500;
int safe_rpm = std::clamp(raw_rpm, kMinRPM, kMaxRPM); // C++17 — returns 8000
// Apply clamp across a range using std::transform
std::vector<int> readings = {200, 3000, 9500, 1500, 750};
std::transform(readings.begin(), readings.end(), readings.begin(),
[](int v) { return std::clamp(v, kMinRPM, kMaxRPM); }); // C++17Best Practices
Prefer minmax_element over separate min_element + max_element calls. The combined algorithm makes only ⌊3(n−1)/2⌋ comparisons versus 2(n−1) for two separate passes — a meaningful difference on large ranges.
Use std::ranges::min/max in new C++20 code. The ranges versions accept the container directly, support projections natively, and return values rather than iterators for scalar uses — less ceremony and harder to misuse.
For compile-time bounds, constexpr std::min/max compose cleanly with template parameters. Since C++14 these are constexpr, so they work inside static_assert, array bounds, and other constant expressions.
template<std::size_t N, std::size_t M>
void copy_safe(std::array<int, N>& dst, const std::array<int, M>& src) {
constexpr std::size_t limit = std::min(N, M); // C++14 constexpr
std::copy_n(src.begin(), limit, dst.begin());
}Reach for std::clamp over hand-rolled min(max(v, lo), hi). The intent is clearer, it is constexpr from C++17, and it has a comparator overload for types without operator<.
Common Pitfalls
Dangling references from temporaries. The two-argument overloads of std::min/std::max return const T&. Binding a temporary result to an auto variable is undefined behavior:
// UB — reference to temporary expires immediately
const int& bad = std::min(compute_a(), compute_b());
*bad; // accessing destroyed temporary
// Safe — copy the result
int good = std::min(compute_a(), compute_b());The initializer-list overloads return by value and are therefore safe with temporaries.
Dereferencing end() on empty ranges. min_element and max_element return the last iterator when the range is empty. Dereferencing it is undefined behavior — always check before use.
std::vector<int> v;
// auto it = std::min_element(v.begin(), v.end());
// *it; // UB — v is empty
if (auto it = std::min_element(v.begin(), v.end()); it != v.end())
use(*it);Passing lo > hi to std::clamp is undefined behavior. The precondition lo <= hi (or that comp(hi, lo) is false) is not checked at runtime in most implementations.
// UB — lo and hi are swapped
int x = std::clamp(value, 100, 10);
// Fix — normalise bounds first
int lo = std::min(bound_a, bound_b);
int hi = std::max(bound_a, bound_b);
int x = std::clamp(value, lo, hi);Mixed signed/unsigned comparisons. std::min(size_t_value, int_value) will not compile — the template deduction conflict is a safety feature, not a nuisance. Cast explicitly and document the truncation intent.
std::minmax stability. When elements are equal, std::minmax(a, b) guarantees the first element of the pair is not greater than the second — but for initializer lists with duplicates, the minimum position is the first occurrence and the maximum is the last. std::minmax_element makes the same guarantee: on equal elements, .first points to the earliest occurrence and .second to the latest.
std::minmax and std::minmax_element
std::minmax finds both extremes in a single expression, returning a std::pair whose .first is the minimum and .second is the maximum. This avoids two separate calls and, for ranges, beats two independent passes on comparison count.
#include <algorithm>
#include <cassert>
int a = 7, b = 3;
// C++11 — returns std::pair<const int&, const int&>
auto p = std::minmax(a, b);
// p.first == 3, p.second == 7
// C++17 structured bindings — reads more naturally
auto [lo, hi] = std::minmax(a, b); // C++14 (constexpr) + C++17 (bindings)
assert(lo == 3 && hi == 7);
// C++11 — initializer-list overload returns pair<T, T> by value (safe with temporaries)
auto [mn, mx] = std::minmax({4, 1, 9, 2, 7}); // C++11 + C++17
assert(mn == 1 && mx == 9);For ranges, std::minmax_element makes at most ⌊3(n−1)/2⌋ comparisons, compared to 2(n−1) for two separate min_element + max_element calls — roughly a 25 % reduction on large ranges.
#include <algorithm>
#include <vector>
std::vector<int> scores = {85, 93, 78, 90, 96, 82};
// C++11 — single pass, pair of iterators
auto [lo_it, hi_it] = std::minmax_element(scores.begin(), scores.end()); // C++11
// Equivalent but costs ~2× as many comparisons:
auto min_it = std::min_element(scores.begin(), scores.end()); // C++98
auto max_it = std::max_element(scores.begin(), scores.end()); // C++98Both iterator members are valid only while the range is not modified, and an empty range returns {last, last} — guard with !scores.empty() before dereferencing.
Clamping: std::clamp (C++17)
std::clamp(v, lo, hi) returns lo if v < lo, hi if v > hi, and v otherwise — equivalent to std::max(lo, std::min(v, hi)) but more expressive and carries a comparator overload. Its precondition is !(hi < lo); violating it is undefined behavior.
#include <algorithm> // C++17
// Normalize a raw sensor reading to a safe operating window
constexpr int kMinRPM = 500, kMaxRPM = 8000;
int raw = 9500;
int safe = std::clamp(raw, kMinRPM, kMaxRPM); // C++17 — returns 8000
// Custom comparator for types with non-default ordering
struct Pressure { double bar; };
auto by_bar = [](const Pressure& x, const Pressure& y) { return x.bar < y.bar; };
Pressure reading{12.5}, lo_p{1.0}, hi_p{10.0};
Pressure clamped = std::clamp(reading, lo_p, hi_p, by_bar); // C++17On floating-point types, if v is NaN the result is unspecified by the standard — NaN propagates through < comparisons in implementation-defined ways, so validate inputs before clamping if NaN is possible.
// UB — lo > hi violates precondition
// int bad = std::clamp(value, 100, 10);
// Fix — normalise bounds before calling clamp
int lo = std::min(bound_a, bound_b); // C++98
int hi = std::max(bound_a, bound_b); // C++98
int ok = std::clamp(value, lo, hi); // C++17Ranges min, max, and minmax (C++20)
The std::ranges versions of min/max accept a range object directly and support projections, eliminating the boilerplate of passing begin/end pairs and wrapping member access in a comparator lambda. They are defined in <algorithm> alongside the classic versions.
#include <algorithm>
#include <vector>
std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};
// C++20 — no begin/end pair required
int lo = std::ranges::min(vec); // C++20 — returns 1
int hi = std::ranges::max(vec); // C++20 — returns 9
// Returns an iterator, not a value
auto it = std::ranges::min_element(vec); // C++20
// Classic equivalent
auto it2 = std::min_element(vec.begin(), vec.end()); // C++98Projections let you express "min by field" without a comparator lambda wrapping member access.
struct Person { std::string name; int age; };
std::vector<Person> team = {{"Alice", 34}, {"Bob", 29}, {"Carol", 41}};
// C++20 — projection extracts the field to compare on
auto youngest = std::ranges::min_element(team, {}, &Person::age); // C++20
auto oldest = std::ranges::max_element(team, {}, &Person::age); // C++20
// ranges::clamp also accepts a range-style comparator and projection
int clamped = std::ranges::clamp(35, 0, 100); // C++20std::ranges::min on an empty range is undefined behavior — unlike min_element, it does not return an end iterator, so ensure the range is non-empty before calling the value-returning overload.
Custom Comparators for Complex Types
Every min/max algorithm that accepts a Compare parameter requires that comparator to define a strict weak ordering — the same contract as std::sort. Violating this (e.g., using <= instead of <) produces undefined behavior.
#include <algorithm>
#include <vector>
struct Task {
std::string name;
int priority; // higher number = higher priority
int deadline; // days until due
};
std::vector<Task> queue = {
{"Deploy", 3, 5},
{"Hotfix", 5, 1},
{"Refactor", 1, 30}
};
// Find the task with the earliest deadline
auto urgent = std::min_element(queue.begin(), queue.end(),
[](const Task& x, const Task& y) { return x.deadline < y.deadline; });
// *urgent == {"Hotfix", 5, 1}
// Find the highest-priority task
auto top = std::max_element(queue.begin(), queue.end(),
[](const Task& x, const Task& y) { return x.priority < y.priority; });
// Two-argument max with a custom comparator
Task a{"Deploy", 3, 5}, b{"Hotfix", 5, 1};
Task& higher = std::max(a, b,
[](const Task& x, const Task& y) { return x.priority < y.priority; });For heterogeneous comparisons (e.g., comparing std::string keys against std::string_view without constructing a temporary), use std::less<> — the transparent comparator that forwards to operator< without fixing T.
// std::less<> is the transparent comparator — works with mixed but comparable types
auto cmp = std::less<>{};
bool result = cmp(std::string_view{"abc"}, std::string{"abd"}); // C++14Integer Overflow in min/max of Numeric Computations
The classic binary-search midpoint (lo + hi) / 2 overflows when both lo and hi are large positive int values, producing a negative intermediate result and a wrong midpoint. This bug has appeared in production standard-library implementations.
// Classic bug — overflows for large lo and hi
int lo = 2'000'000'000, hi = 2'100'000'000;
int mid_bad = (lo + hi) / 2; // UB: signed overflow
// Safe manual fix — no overflow because (hi - lo) fits in int here
int mid_ok = lo + (hi - lo) / 2;C++20 introduced std::midpoint and std::lerp in <numeric> to handle these cases correctly and portably.
#include <numeric> // C++20
// std::midpoint<T> — rounds toward lo, handles signed/unsigned/pointer/float
int mid = std::midpoint(lo, hi); // C++20 — safe, no overflow
// std::lerp(a, b, t) — returns a + t*(b-a) without intermediate overflow
double interp = std::lerp(0.0, 1.0, 0.5); // C++20 — returns 0.5 exactly
// lerp is monotonic and guarantees lerp(a,b,0)==a and lerp(a,b,1)==b
float start = 100.0f, end = 200.0f;
for (float t = 0.0f; t <= 1.0f; t += 0.1f)
float v = std::lerp(start, end, t); // C++20Prefer std::midpoint over the manual lo + (hi - lo) / 2 idiom in new code: it also handles pointer arithmetic and floating-point midpoints with correct rounding.
SIMD-Friendly min/max with std::execution
Parallel reductions over large containers are most cleanly expressed with std::reduce or std::transform_reduce together with an execution policy, letting the implementation vectorize or distribute the work across threads. The sequential std::min_element cannot be parallelized because it must return an iterator to a specific position; a parallel min that only needs the value is a better fit.
#include <algorithm>
#include <numeric>
#include <execution> // C++17
#include <vector>
#include <climits>
std::vector<int> data(1'000'000);
// ... fill data ...
// C++17 — parallel minimum value (not position)
int pmin = std::reduce(
std::execution::par_unseq, // C++17 — parallel + vectorized
data.begin(), data.end(),
INT_MAX,
[](int x, int y) { return x < y ? x : y; }
);
// Alternatively, using std::min<int>{} as the binary op
int pmin2 = std::reduce(
std::execution::par_unseq, // C++17
data.begin(), data.end(),
INT_MAX,
std::min<int>{}
);
// transform_reduce — find the minimum absolute value in parallel
int min_abs = std::transform_reduce(
std::execution::par_unseq, // C++17
data.begin(), data.end(),
INT_MAX,
[](int x, int y) { return x < y ? x : y; }, // reduction
[](int x) { return std::abs(x); } // transformation
);Note that std::min<int> used as a binary op in std::reduce is associative and commutative for integers — the standard requires the binary op in std::reduce to be associative (re-ordering of operations is allowed), so floating-point std::min<float> is technically permitted but may yield different rounding results from the sequential version. For the parallel execution policies to be available, link against a TBB or similar parallel backend on platforms where the standard library does not bundle one.
See Also
std::nth_element— partial sort that puts the nth-smallest element in position without fully sortingstd::sort/std::partial_sort— when the full ordering matters, not just the extremesstd::accumulate/std::reduce— fold operations that can generalise min/max viastd::minas the binary op