How to Constrain Numeric Values with C++ std::clamp
In modern C++, constraining a numeric value within an acceptable
range is a common requirement that previously required nested ternary
operators or verbose std::min and std::max
calls. Introduced in C++17 within the <algorithm>
header, std::clamp provides a clean, standard, and
expressive solution for range-bounding. This article explains how
std::clamp functions, details its syntax and internal
logic, provides practical code examples, and highlights critical safety
considerations to avoid undefined behavior.
Syntax and Parameters
The standard library provides two overloads for
std::clamp:
// 1. Default comparator using operator<
template<class T>
constexpr const T& clamp(const T& v, const T& lo, const T& hi);
// 2. Custom comparator
template<class T, class Compare>
constexpr const T& clamp(const T& v, const T& lo, const T& hi, Compare comp);v: The value to constrain.lo: The lower bound.hi: The upper bound.comp: An optional comparison function or lambda returningtrueif the first argument is strictly less than the second.
How the Clamping Logic Works
The function enforces that the returned value sits within the closed
interval [lo, hi]. The operation evaluates the inputs
sequentially using the less-than operator (or the provided
comparator):
- Check Lower Bound: If
v < lo, it returns a reference tolo. - Check Upper Bound: If
hi < v, it returns a reference tohi. - Within Range: If neither condition is true, it
returns a reference to
v.
Because the return type is a constant reference
(const T&), std::clamp avoids unnecessary
copying of complex objects. Both overloads are marked
constexpr, making them fully usable at compile time.
Code Example
#include <iostream>
#include <algorithm>
int main() {
int min_val = 0;
int max_val = 100;
// Below the lower bound
int low = std::clamp(-15, min_val, max_val); // Returns 0
// Within the bounds
int mid = std::clamp(42, min_val, max_val); // Returns 42
// Above the upper bound
int high = std::clamp(150, min_val, max_val); // Returns 100
std::cout << "Low: " << low << "\n";
std::cout << "Mid: " << mid << "\n";
std::cout << "High: " << high << "\n";
return 0;
}Important Considerations and Edge Cases
1. Inverted Bounds Cause Undefined Behavior
A critical precondition for std::clamp is that
lo must be less than or equal to hi
(lo <= hi). If lo > hi, the behavior is
undefined. The function does not automatically swap boundaries if they
are passed in reverse order.
2. Lifetime of Temporaries
Because std::clamp returns a reference to one of its
input arguments, caution is necessary when passing temporary values
(rvalues). Binding the return value to a reference can create a dangling
reference if the temporary goes out of scope:
// Dangerous: dangling reference
const int& ref = std::clamp(10, 0, 100);
// Safe: copied by value
int val = std::clamp(10, 0, 100); 3. Handling Floating-Point NaN
When clamping floating-point values (float,
double), the presence of NaN (Not a Number)
produces non-standard results. Because all comparisons with
NaN return false, passing NaN as
v typically causes std::clamp to return
NaN. If lo or hi is
NaN, the function may violate the internal ordering
invariant and yield unpredictable output.