Skip to content
C++
Library
since C++98
Basic

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++98

A 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:

FunctionIntroducedOperates on
std::min, std::maxC++98two values
Initializer-list overloadsC++11{a, b, c, …}
std::minmax, std::minmax_elementC++11values / ranges
std::clampC++17single value against bounds
std::ranges::min, ranges::max, etc.C++20ranges 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

cpp
// 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 exactly

Combined min and max

cpp
// 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

cpp
// 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)

cpp
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

cpp
// 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

cpp
#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});  // 8

Type mismatch — a common trap

cpp
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 type

Range algorithms

cpp
#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++11

Custom comparator

cpp
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

cpp
// 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 96

std::clamp (C++17)

cpp
#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++17

Best 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.

cpp
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:

cpp
// 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.

cpp
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.

cpp
// 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.

cpp
#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.

cpp
#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++98

Both 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.

cpp
#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++17

On 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.

cpp
// 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++17

Ranges 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.

cpp
#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++98

Projections let you express "min by field" without a comparator lambda wrapping member access.

cpp
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++20

std::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.

cpp
#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.

cpp
// 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++14

Integer 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.

cpp
// 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.

cpp
#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++20

Prefer 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.

cpp
#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 sorting
  • std::sort / std::partial_sort — when the full ordering matters, not just the extremes
  • std::accumulate / std::reduce — fold operations that can generalise min/max via std::min as the binary op