[FR] small change to Range to support more numeric types (e.g. Time)

Currently, Range assumes that the difference between two ValueTypes is still a ValueType.

That certainly is true for all primitive numeric types (int, float, etc.), but there are cases where that is not the case. One such example is the Time class in JUCE, where:

Time - Time -> RelativeTime

And yet a Range <Time> object would totally make sense: it would work out of the box for all the functionality to check if another Time object is within range, etc.

That breaks where Range tries to fit the difference of two Time objects in another Time object, for example:

constexpr inline ValueType getLength() const noexcept { return end - start; }

what I propose is to add a second DistanceType parameter to the Range, which defaults to the type that results as the difference between two ValueTypes.

template <typename ValueType, 
          typename DistanceType = decltype (ValueType() - ValueType ())>
class Range
...

and use it as the type of the differences between ValueTypes (as in getLength() above) and as the type of the offsets to be applied to ValueTypes, for example:

[[nodiscard]] constexpr Range expanded (DistanceType amount) const noexcept
{
    return Range (start - amount, end + amount);
}

None of the expressions currently used in Range need any change. The only change is in the type of some parameters and return values.

To make things safer, the required relationship between ValueType and DistanceType can be formalized with two static asserts:

// requirement #1: ValueType - ValueType -> DistanceType
static_assert (std::is_convertible <decltype (ValueType () - ValueType()), DistanceType>::value);

// requirement #2: ValueType + DistanceType -> ValueType
static_assert (std::is_convertible <decltype (ValueType () + DistanceType()), ValueType>::value);

May not be applicable in all cases, but if you use an auto return type, you shouldn’t need the second template argument:

constexpr inline auto getLength() const noexcept { return end - start; }

Then for the methods that take the distance type, you could template them allowing any type to be passed in (and relying on a compilation failure if you pass in a type that the ValueType doesn’t support):

template <typename DistanceType>
Range operator+= (const DistanceType& toAdd) noexcept;

That way you don’t have to manually specify the distance type, it’ll always be deduced for you by the compiler.

Thank you for your feedback.
Yes, I considered suggesting the usage of auto and of function template to make the code accept anything that compiles.
Besides, if we were in C++20 land, one could also use auto for the function parameters, like:

Range operator+= (const auto& toAdd) noexcept;

But I must admit that for now I prefer the idea of an explicit DistanceType, because in that case the contract that must be satisfied by the types involved can be expressed explicitly with the two static_asserts.

With those in mind, the implementor of Range can write the body of its methods in a way that satisfies the contract, for example:

    return Range (start + (newEnd - end), newEnd);

respects the contract because:
newEnd - end -> DistanceType
and then
start + DistanceType -> ValueType

whereas the following formulation, equivalent on the surface, would break it:

    return Range (start + newEnd - end, newEnd);

because it uses ValueType + ValueType that is not in the contract.

In other words, with the usage auto, the “contract” would be implicit in the way the code is written inside the body of those methods. In a certain sense, the contract would not be clearly stated but would emerge from the way the code is written, which is an implementation detail and is subject to change