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 returning true if 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):

  1. Check Lower Bound: If v < lo, it returns a reference to lo.
  2. Check Upper Bound: If hi < v, it returns a reference to hi.
  3. 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.