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.

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