Minimum and Maximum Algorithms

An introduction to the 7 minimum and maximum algorithms in the C++ standard library: clamp(), min(), min_element(), max(), max_element(), minmax(), and minmax_element().
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated

In this lesson, we introduce the minimum and maximum algorithms available within the C++ standard library.

All the algorithms in this section are available within the <algorithm> header:

#include <algorithm>

We will cover clamp(), min(), min_element(), max(), max_element(), minmax(), and minmax_element().

std::ranges::clamp()

The std::ranges::clamp() function is used to ensure a value is within a specific range.

The function takes three arguments, the value to clamp, the minimum allowed value, and the maximum allowed value

  • If the value to clamp is below the minimum, clamp() returns the minimum
  • If the value to clamp is above the maximum, clamp() returns the maximum
  • If the value is between the minimum and maximum, clamp() returns the value

The following shows an example of clamping numbers to the 0-255 range:

#include <algorithm>
#include <iostream>

int main() {
  using std::ranges::clamp;

  std::cout
    << "Clamp -30: " << clamp(-30, 0, 255)
    << "\nClamp 100: " << clamp(100, 0, 255)
    << "\nClamp 300: " << clamp(300, 0, 255);
}
Clamp -30: 0
Clamp 100: 100
Clamp 300: 255

Return Type

The clamp() algorithm, and most other algorithm in this lesson, will return a const reference to the chosen element:

#include <algorithm>
#include <iostream>

int main() {
  int Value { 40 };

  const int& ClampedValue {
    std::ranges::clamp(Value, 0, 255)
  };

  std::cout << "Value:        " << &Value << '\n'
            << "ClampedValue: " << &ClampedValue;
}
Value:        00000054AC2FFC14
ClampedValue: 00000054AC2FFC14

If preferred, we can create a copy of the returned value:

#include <algorithm>
#include <iostream>

int main() {
  int Value { 40 };

  int ClampedValue {
    std::ranges::clamp(Value, 0, 255)
  };

  std::cout << "Value:        " << &Value << '\n'
            << "ClampedValue: " << &ClampedValue;
}
Value:        000000414C36FC44
ClampedValue: 00000040F50FF7A4

std::ranges::min()

The std::ranges::min() algorithm can return the minimum result of two expressions.

#include <algorithm>
#include <iostream>

int main() {
  int Result{std::ranges::min(1, 10)};  
  std::cout << "The min value is: " << Result;
}
The min value is: 1

Naturally, this is only useful if we don’t know which value will be the minimum - that is, where one or more of the arguments is a more complex expression such as a function call. Instead of writing logic like this:

int MyFunction() {
  int A{FuncA()};
  int B{FuncB()};
  return A < B ? A : B;
}

We can do write this, which is simpler and more descriptive:

int MyFunction() {
  return std::ranges::min(FuncA(), FuncB());
}

Finding the Minimum Value in a Range

An overload of std::ranges::min() allows us to find the minimum value in a range of candidates:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers { 2, 3, 1, 4, 5 };
  int Result { std::ranges::min(Numbers) };

  std::cout << "Smallest number: " << Result;
}
Smallest number: 1

If multiple objects are tied for the minimum, the algorithm will return the first (ie, leftmost) one in the range.

Using Subranges / Iterator-Sentinel Pairs

Unlike most range-based algorithms, std::ranges::min() does not allow us to specify our range as an iterator-sentinel pair as of C++23. However, we can bypass this with minimal performance overhead by creating a view (such as a subrange) from our iterator and sentinel, and then providing that view as a single argument to std::ranges::min()

Below, we find the minimum value of a collection, excluding the first and last elements:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers{2, 3, 5, 4, 1};

  int Result{std::ranges::min(
    std::ranges::subrange(
      Numbers.begin() + 1, Numbers.end() - 1
    ))};

  std::cout << "Smallest number: " << Result;
}
Smallest number: 3

std::ranges::min_element()

The min_element() function works with ranges in much the same way as min(). The key difference is that, whereas min() will return a reference to the smallest object, min_element() will return an iterator pointing to it.

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers{2, 3, 1, 4, 5};
  auto Result{std::ranges::min_element(Numbers)};  

  std::cout << "The min element is at index: "
    << std::distance(Numbers.begin(), Result);

  std::cout << "\nIts value is: " << *Result;
}
The min element is at index: 2
Its value is: 1

In this example, we use this to insert a 0 before our minimum element, using the emplace() method provided by our std::vector:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers { 2, 3, 1, 4, 5 };
  auto Result { std::ranges::min_element(Numbers) };

  Numbers.emplace(Result, 0);

  for (int& i : Numbers) {
    std::cout << i << ", ";
  }
}
2, 3, 0, 1, 4, 5,

Using Iterator-Sentinel Pairs

The std::ranges::min_element() algorithm allows us to provide our range as an iterator-sentinel pair. Below, we use this to find the minimum element in our collection, excluding the first and last element:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers{1, 3, 5, 4, 2};
  auto Result{std::ranges::min_element(
    Numbers.begin() + 1, Numbers.end() - 1)};  

  std::cout << "The min element is at index: "
    << std::distance(Numbers.begin(), Result);

  std::cout << "\nIts value is: " << *Result;
}
The min element is at index: 1
Its value is: 3

std::ranges::max()

Similar to std::ranges::min(), the simplest use of std::ranges::max() has us pass two values, and it returns which value is the greatest:

#include <algorithm>
#include <iostream>

int main() {
  int Result{std::ranges::max(1, 10)};  
  std::cout << "The max value is: " << Result;
}
The max value is: 10

This is typically used to place a lower bound on some unknown value. The rectified linear unit (ReLU) function is an example of this. It receives a value and returns that same value if it’s positive, or 0 otherwise. This is widely used in machine learning contexts and could be implemented using std::ranges::max() like this:

#include <algorithm>
#include <iostream>

int ReLU(int Value) {
  return std::ranges::max(0, Value);
}

int main() {
  std::cout << "ReLU(-10): " << ReLU(-10);
  std::cout << "\nReLU(10): " << ReLU(10);
}
ReLU(-10): 0
ReLU(10): 10

Finding the Maximum Value in a Range

The maximum value in a range can be retrieved using the max() algorithm:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers { 2, 3, 1, 4, 5 };
  const int& Result { std::ranges::max(Numbers) };

  std::cout << "Largest number: " << Result;
}
Largest number: 5

If multiple objects are tied for the maximum, the algorithm will return the first (ie, leftmost) one in the range.

Using Subranges / Iterator-Sentinel Pairs

Similar to std::ranges::min() , the std::ranges::max() algorithm does not currently allow us to specify our range as an iterator-sentinel pair. We can circumvent this with minimum performance impact by using our desired iterator and sentinel to create an intermediate view.

Below, we find the maximum value of a collection, excluding the first and last elements:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers{5, 3, 2, 1, 4};

  int Result{std::ranges::max(
    std::ranges::subrange(
      Numbers.begin() + 1, Numbers.end() - 1
    ))};

  std::cout << "Largest number: " << Result;
}
Largest number: 3

std::ranges::max_element()

Just as min_element() returns an iterator to the smallest object in our range, the max_element() algorithm returns an iterator pointing to the largest:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers{2, 3, 1, 4, 5};
  auto Result{std::ranges::max_element(Numbers)};  

  std::cout << "The max element is at index: "
    << std::distance(Numbers.begin(), Result);

  std::cout << "\nIts value is: " << *Result;
}
The max element is at index: 4
Its value is: 5

Here, we use the iterator to insert a 0 before our maximum element:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers { 2, 3, 1, 4, 5 };
  auto Result { std::ranges::max_element(Numbers) };

  Numbers.emplace(Result, 0);

  for (int& i : Numbers) {
    std::cout << i << ", ";
  }
}
2, 3, 1, 4, 0, 5,

Using Iterator-Sentinel Pairs

We can provide our range to std::ranges::max_element() as an iterator-sentinel pair if needed. Below, we use this technique to find the maximum value in a collection, excluding the first and last elements:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers{2, 3, 1, 4, 5};
  auto Result{std::ranges::max_element(  
    Numbers.begin() + 1, Numbers.end() - 1)};  

  std::cout << "The max element is at index: "
    << std::distance(Numbers.begin(), Result);

  std::cout << "\nIts value is: " << *Result;
}
The max element is at index: 3
Its value is: 4

std::ranges::minmax()

As the name suggests, the minmax() function is a combination of the min() and max() functions. In its most basic usage, we provide two arguments, and it returns a simple struct. The smaller of the two arguments will be in the min member variable, whilst the larger will be in the max member variable:

#include <algorithm>
#include <iostream>

int main() {
  auto Result{std::ranges::minmax(10, 1)};  

  std::cout << "Min: " << Result.min
    << ", Max: " << Result.max;
}
Min: 1, Max: 10

We’d typically use structured binding with this return type:

#include <algorithm>
#include <iostream>

int main() {
  auto [min, max]{std::ranges::minmax(10, 1)};  

  std::cout << "Min: " << min
    << ", Max: " << max;
}
Min: 1, Max: 10

Using minmax() on a range

We can retrieve both the minimum and maximum values in a range at once using the minmax() algorithm. This algorithm returns a struct with two properties: min and max:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers { 2, 3, 1, 4, 5 };
  const auto& Result { std::ranges::minmax(Numbers) };

  std::cout << "Smallest: " << Result.min;
  std::cout << "\nLargest: " << Result.max;
}
Smallest: 1
Largest: 5

If multiple objects are tied for the minimum, the min property will contain the first (ie, the leftmost) one in the range

If multiple objects are tied for the maximum, the max property will contain the last (ie, the rightmost) one in the range

We’d also typically use structured binding to access the return values from this overload of minmax():

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers { 2, 3, 1, 4, 5 };
  const auto& [min, max] {
    std::ranges::minmax(Numbers)
  };

  std::cout << "Smallest number: " << min;
  std::cout << "\nLargest number: " << max;
}
Smallest number: 1
Largest number: 5

Using Iterator-Sentinel Pairs

Because the two-argument overload of std::ranges::minmax() is reserved for the initial use case where we provide two values, we can’t currently provide a range as an iterator-sentinel pair.

However, as with previous examples, we can easily implement this behavior when needed by introducing an intermediate view. Below, we find the minimum and maximum elements of our collection, excluding the first and last elements:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers{1, 3, 2, 4, 5};

  auto[min, max]{std::ranges::minmax(
    std::ranges::subrange{  
      Numbers.begin() + 1, Numbers.end() - 1}  
  )};

  std::cout << "Min: " << min
    << ", Max: " << max;
}
Min: 2, Max: 4

std::ranges::minmax_element()

Finally, we have minmax_element(), which will return iterators that point to the minimum and maximum objects of our range:

#include <algorithm>
#include <iostream>
#include <list>

int main() {
  std::list Numbers { 2, 3, 1, 4, 5 };
  auto [min, max] {
    std::ranges::minmax_element(Numbers)
  };

  std::cout << "Min element: " << *min
    << ", Index: "
    << std::distance(Numbers.begin(), min);

  std::cout << "\nMax element: " << *max
    << ", Index: "
    << std::distance(Numbers.begin(), max);
}
Min element: 1, Index: 2
Max element: 5, Index: 4

Similar to other _element variations of these algorithms, this is mostly used if our follow-up logic is based on where the minimum and maximum elements were found in the collection.

Below, we use the iterators returned from minmax_element() to insert a 0 before both our minimum and maximum elements:

#include <algorithm>
#include <iostream>
#include <list>

int main() {
  std::list Numbers { 2, 3, 1, 4, 5 };
  auto [min, max] {
    std::ranges::minmax_element(Numbers)
  };

  Numbers.insert(min, 0);
  Numbers.insert(max, 0);

  for (int& i : Numbers) {
    std::cout << i << ", ";
  }
}
2, 3, 0, 1, 4, 0, 5,

Using Iterator-Sentinel Pairs

As usual, we can provide our range as an iterator-sentinel pair rather than as a single argument. Below, we use this technique to find the minimum and maximum elements in our collection, excluding the first and last objects:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector Numbers { 5, 3, 2, 4, 1 };
  auto [min, max] {std::ranges::minmax_element(
    Numbers.begin() + 1, Numbers.end() - 1)};

  std::cout << "Min element: " << *min
    << ", Index: "
    << std::distance(Numbers.begin(), min);

  std::cout << "\nMax element: " << *max
    << ", Index: "
    << std::distance(Numbers.begin(), max);
}
Min element: 2, Index: 2
Max element: 4, Index: 3

Type Requirements

For types to be directly supported by minimum and maximum algorithms, they generally need to implement all six of the comparison operators: >, <, >=, <=, ==, and !=. Below, we define a compatible type:

#include <algorithm>
#include <iostream>
#include <vector>

struct MyStruct {
  int Value;
  bool operator<(const MyStruct& Other) const {
    return Value < Other.Value;
  }
  bool operator<=(const MyStruct& Other) const {
    return Value <= Other.Value;
  }
  bool operator>(const MyStruct& Other) const {
    return Value > Other.Value;
  }
  bool operator>=(const MyStruct& Other) const {
    return Value >= Other.Value;
  }
  bool operator==(const MyStruct& Other) const {
    return Value == Other.Value;
  }
  bool operator!=(const MyStruct& Other) const {
    return Value != Other.Value;
  }
};

int main() {
  std::vector Numbers {
    MyStruct { 2 },
    MyStruct { 3 },
    MyStruct { 1 },
    MyStruct { 4 },
    MyStruct { 5 }
  };

  const MyStruct& min {
    std::ranges::min(Numbers) };

  std::cout << "Smallest: " << min.Value;
}
Smallest: 1

C++20 Expression Rewriting

C++20’s expression rewriting feature makes it easier to implement comparison operators for our custom types. Rather than needing to define all 6 operators as we did above, we only need to define two - the == operator and the "spaceship operator" <=>

We cover expression rewriting and the spaceship operator in more detail later in the course.

If we have C++20 available and want to take advantage of expression rewriting, our previous example can be simplified to this:

#include <algorithm>
#include <iostream>
#include <vector>

struct MyStruct {
  int Value;
  bool operator==(const MyStruct& Other) const {
    return Value == Other.Value;
  }
  std::strong_ordering operator<=>(
    const MyStruct& Other) const {
    return Value <=> Other.Value;
  }
};

int main() {
  std::vector Numbers {
    MyStruct { 2 },
    MyStruct { 3 },
    MyStruct { 1 },
    MyStruct { 4 },
    MyStruct { 5 }
  };

  const MyStruct& min {
    std::ranges::min(Numbers) };

  std::cout << "Smallest: " << min.Value;
}
Smallest: 1

Custom Comparison Functions

The algorithms we covered in this lesson allow us to provide a custom comparison function. This function will accept two objects of the appropriate type and should return true if the first object is "smaller" than the second.

When providing a custom comparison function, that also means the type stored within our collection doesn’t need to have comparison operators implemented, as the algorithm will be using our comparison function instead of those operators.

In the following example, we use a custom comparison function to find out which player has the lowest health:

#include <algorithm>
#include <iostream>
#include <vector>

class Player {/*...*/}; int main() { std::vector Party{ Player{"Roderick", 100}, Player{"Anna", 200}, Player{"Robert", 500} }; auto Comparer { [](Player& A, Player& B) { return A.GetHealth() < B.GetHealth(); } }; const Player& LowestHealth{ std::ranges::min(Party, Comparer) }; std::cout << "Lowest Health: " << LowestHealth.GetName(); }
Lowest Health: Roderick

Projection Functions

All the algorithms in this lesson accept an additional optional third argument, which can be a projection function. Projection is a standard feature of range-based algorithms, and we cover it in more detail here:

In the following example, we use std::abs() as a projection function for the min() algorithm, to find the number in our collection with the lowest absolute value. Because we want to use the default comparison function, we pass {} in that position:

#include <algorithm>
#include <iostream>
#include <vector>

int Projector(int x) { return std::abs(x); }

int main() {
  std::vector Nums {-10, -3, 1, 4};

  int Min{std::ranges::min(Nums, {}, Projector)};

  std::cout << "Minimum Absolute Value: " << Min;
}
Minimum Absolute Value: 1

In the following example, we find the Player with the lowest health, by using a Player method as the projection function:

#include <algorithm>
#include <iostream>
#include <vector>

class Player {/*...*/}; int main() { std::vector Party { Player{"Roderick", 100}, Player{"Anna", 200}, Player{"Robert", 500} }; const Player& LowestHealth{std::ranges::min( Party, {}, &Player::GetHealth)}; std::cout << "Lowest Health: " << LowestHealth.GetName(); }
Lowest Health: Roderick

Using Pre-Range Algorithms

The algorithms covered in this chapter are using the <ranges> library, introduced in C++20. When targeting specifications before C++20, there are older variations of all of these functions. They are available by excluding the ranges specification.

For example, instead of using std::ranges::min(), we can use std::min():

#include <algorithm>
#include <iostream>

int main() {
  int Result{std::min(1, 10)};  
  std::cout << "The min value is: " << Result;
}
The min value is: 1

These older variations have some weaknesses:

  • They don’t support projection
  • The overloads that work on collections tend to expect the collection to be provided as a std::initializer_list, which is less convenient ranges
  • The overloads that directly accept iterators require the end to be defined by an iterator, rather than the more flexible sentinel pattern

However, use of the older algorithms remains common, and may even be preferred for simple use cases like the previous std::min() example.

Summary

In this lesson, we explored the seven key min/max algorithms offered by the C++ standard library. The main points we learned include:

  • The functionality and application of clamp(), min(), min_element(), max(), max_element(), minmax(), and minmax_element() within the <algorithm> header.
  • Implementing custom comparison functions to refine how algorithms determine the 'smaller' or 'larger' elements.
  • The type requirements for objects manipulated by these algorithms, with examples of how to implement these requirements using comparison operators or the C++20 spaceship operator for custom types.
  • Applying projection functions to transform elements before comparison.
  • The differences between the C++20 <ranges> variations of these algorithms and their earlier, non-range counterparts.

Was this lesson useful?

Next Lesson

Movement Algorithms

An introduction to the 7 movement algorithms in the C++ standard library: move(), move_backward(), rotate(), reverse(), shuffle(), shift_left(), and shift_right().
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Standard Library Algorithms
Next Lesson

Movement Algorithms

An introduction to the 7 movement algorithms in the C++ standard library: move(), move_backward(), rotate(), reverse(), shuffle(), shift_left(), and shift_right().
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved