JoshJers' Ramblings

Infrequently-updated blog about software development, game development, and music

Posts tagged “c++”

Protecting Coders From Ourselves: Min, Max, Lerp, and Clamp

Imagine this: you’ve got some value, x, that you want to ensure is at least 1. That is to say, you want to ensure its minimum value is 1. So, being the smart, experienced programmer that you are, you write the following:

x = Min(x, 1);

You give yourself the small, satisfied nod of a job well done and run the program and then it all goes immediately sideways because that should have been Max and not Min.

If you’ve been writing code for basically any length of time, the above was probably less an “imagine this” and more a “remember this” because if you’re anything like me, you’ve done this over. and over. and over.

Inspired by once again mistakenly using Min instead of Max to limit the minimum allowed value of something, I’ve decided to start a little series (will it have more than one entry? who knows!) called Protecting Coders From Ourselves, in which we rework some bit of API surface to make it less error-prone. We’re going to deal with the “I chose the wrong Min/Max again” problem, but first I want to talk about Clamp and Lerp.

Note

The examples here are in C++, but the concepts should be relevant to basically any language.

Clamp and Lerp

Clamp

Clamp is a simple enough function: Take some value v and make sure it is no less than min and no greater than max. Almost every clamp function in every library I’ve seen has three parameters, one of which represents the value to be clamped, and two of which represent the range that it should be clamped within:

Clamp(a, b, c)

The question, of course, is “which parameter is which?” Many languages (ex: C++, C#, HLSL, and Rust) have the following arrangement in their standard libraries:

Clamp(valueToClamp, min, max)

where the first parameter is the value being clamped and the last two are the min and max ends of the range.

But I’ve also seen this one (looking at you, CSS):

Clamp(min, valueToClamp, max)

This one puts the value to clamp in the middle of the range (which, honestly, is conceptually where it belongs).

Lerp

Another common function that takes a value and a range is Lerp, which uses a value in the range [0, 1] to linearly interpolate between two endpoint values. Most lerp functions that I’ve seen (ex: C++, C#, and HLSL) have their parameters ordered as follows:

Lerp(a, b, t)

where a and b are the range endpoints and t is the interpolating value.

Depending on the projects you work on, you maybe don’t write code that uses Lerp very often (or ever), but I do, and for me the combination of Lerp and Clamp are a source of constant, mild confusion.

Parameter Confusion

Both of these functions take three parameters, two of which represent a range and one which is a value that is either limited by or used to interpolate within the range. In isolation it’s easy to rationalize the order of the parameters for each:

  • Clamp(v, min, max): Clamp v to be within the range [min, max].
  • Clamp(min, v, max): Clamp such that min <= v <= max.
  • Lerp(a, b, t): Get a linearly-interpolated value between a and b using t.

…but in combination I am constantly second-guessing which order the parameters need to go in. Sometimes, for instance, I’ll write the equivalent of Lerp(t, a, b) and wonder why nothing is working the way I expect.

That brings us (finally) to the question of the article: how can we make it clear which parameters are which in these functions?

An obvious way to do this, given language support, is to make use of named parameters when calling the function:

Clamp(v=value, min=0, max=5)

but not all languages (looking at you, C++) support named parameters, so what then?

Grouping the Range Values

What if instead of the above, calls looked more like this:

Clamp(a, {b, c});
Lerp({a, b}, c);

With this added structure, it’s clear even without reasonable variable names which part is the range and which is the value, and it’s much more difficult to accidentally call them with the parameters in the wrong order, since, for instance, Lerp(t, {a, b}) wouldn’t even compile.

To do this, we need a simple range structure. in C++ it could look something like this:

template <typename T>
struct ValueRange
{
  // Use a constructor to ensure both endpoints are required.
  ValueRange(T a_, T b_)
    : a{a_}, b{b_} {}

  T a;
  T b;
};

Using this range, then, you could define Clamp and Lerp as follows:

template <typename T>
T Clamp(T v, ValueRange<T> range)
{
  return Max(range.a, Min(range.b, v)); 
}

template <typename T>
T Lerp(ValueRange<T> range, T t)
{
  return range.b * t + range.a * (1 - t);
}

Now instead of three parameters, they take two, which matches how they work conceptually: with a range and a value in some order.

Once you start grouping your input parameters , you may start seeing other places to do it, like IsInRange<Inclusive>(v, {0, 20}).

Min and Max

Back to the Min/Max problem. As stated perfectly by Tom Forsyth:

“Almost every time I use [min or max], I think very carefully and then pick the wrong one.”

This tends to happen because you think “I need to make sure the max value of x is 10” and it just feels right to turn that into Max(x, 10)that’s how it gets you. Or, well, me. That’s how it gets me. Basically every time I have to do this, I will choose the wrong one on the first try, have my code explode on me, and then go back and headdesk at it until it turns into the correct one.

Basically. Every. Time.

Reframing The Problem

But what if you looked at it a different way? What if you instead thought of it as “I want to clamp x so that it’s no larger than 10”? You want something that just clamps one end of it, in a clear way. What if you could declare a one-sided clamp:

// Using "Open" to declare a side of the range is open. Same as:
//  x = Min(x, 10);
x = Clamp(x, {Open, 10});

// Another alternative, ensure that y is no smaller than 2, same as: 
//  y = Max(y, 2);
y = Clamp(y, {2, Open});

To do this efficiently, we’ll have multiple overloads of Clamp, and define two additional “range” structures, each of which takes an OpenEnded_t:

// Declare this as a nice, type-safe enum class
enum class OpenEnded_t
{
  Open,
};

// But make "Open" easy to reach using C++20's "using enum" feature.
using enum OpenEnded_t;

// This is a "range" where only the "a" value is specified, the "b" end is open
template <typename T>
struct ValueRangeOpenB
{
  ValueRangeOpenB(T a_, OpenEnded_t)
    : a(a_) { }
  T a;
};

// Like the above, but it's the "b" end that's specified while "a" is open
template <typename T>
struct ValueRangeOpenA
{
  ValueRangeOpenA(OpenEnded_t, T b_)
    : b(b_) { }
  T b;
};
Note

You can, of course, call Open whatever you’d prefer: I considered many options (including Unbounded, Infinite, OpenEnded, and None), but Open was short and, to my mind, clear.

If you have a global Open function (or you’re in a class that has a function named Open), this likely won’t work. You could add a second enum value called OpenEnded that could be used interchangeably with Open for that case, or just specify it fully qualified, or just pick a less-inconvenient name.

Once you have these structures, you can define two additional overloads of Clamp:

template <typename T>
T Clamp(T v, ValueRangeOpenB<T> range)
  { return Max(v, range.a); }


template <typename T>
T Clamp(T v, ValueRangeOpenA<T> range)
  { return Min(v, range.b); }

These just turn into the correct call to Min or Max, but now you can think of it in terms of limiting one side of its range or the other, rather than trying to A Beautiful Mind your way into picking the correct function right off the bat.

Now, finally, you can limit a value in multiple ways using the same concept, which can make it easier to reason about when you’re writing the code, and also easier to understand when you’re reading it a month later.

// Keep within a range:
x = Clamp(x, {1, 5});

// Limit the lower bound:
y = Clamp(y, {1, Open});

// Limit the upper bound:
z = Clamp(z, {Open, 5});

Final Thoughts

These are ideas I first proposed at my job, and got immediate buy-in from the dev team, because we all kept making the same kinds of mistakes with these functions. There are, of course, times when Min and Max are still the appropriate function to use (like when you’re thinking “I need the minimum of these values”) - but when you’re trying to limit the range of something, Clamp is a clearer declaration of intent.

There are ways to improve these functions:

  • In C++ I highly recommend making all of this constexpr (including the constructors) so that you can use these functions at compile time as well.
    • Depending on your codebase it may be desirable to additionally mark them [[nodiscard]] and noexcept.
    • Also, restricting the template types using C++20 concepts can help give you better error messages if you try to compile it with something that it can’t work with.
  • The full-range Clamp function may want to have some validation that b >= a (perhaps an assert).