Support runtime floating-point clamped_value before C++20 (#1595)

This commit is contained in:
Drew Rife 2026-09-08 14:45:31 -04:00 committed by GitHub
parent 2696828f27
commit ffea27142a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 105 additions and 55 deletions

View File

@ -10,26 +10,27 @@ title: "clamped_value"
Provides a value constrained between a minimum and maximum value. Values are
clamped using `etl::clamp`. The type supports compile-time bounds to minimise
storage and runtime bounds when the range must be changed. Integral types are
supported in all language modes. Floating-point types are supported in C++20
when `ETL_HAS_FLOATING_POINT_CLAMPED_VALUE` is `1`. By default, the macro is
enabled when `__cpp_nontype_template_args` is at least `201911L`.
supported in all language modes. Floating-point types with runtime bounds are
supported in C++11 and later when `ETL_HAS_FLOATING_POINT_CLAMPED_VALUE` is
`1`. Floating-point compile-time bounds require C++20 support for
floating-point non-type template arguments.
## Availability
`ETL_HAS_FLOATING_POINT_CLAMPED_VALUE` is always defined by this header as
either `0` or `1`. It defaults to `1` only when C++20 is enabled and the
compiler supports floating-point non-type template arguments. A platform
profile or build may define the macro before including the header to override
automatic detection.
either `0` or `1`. It defaults to `1` in C++11 and later. The related
`ETL_HAS_COMPILE_TIME_FLOATING_POINT_CLAMPED_VALUE` macro defaults to `1` only
when C++20 is enabled and the compiler supports floating-point non-type
template arguments. A platform profile or build may define either macro before
including the header to override automatic detection.
When the macro is `0`, `T` must be an integral type. When it is `1`, `float`,
`double`, and `long double` may also be used.
When `ETL_HAS_FLOATING_POINT_CLAMPED_VALUE` is `0`, `T` must be an integral
type. When it is `1`, `float`, `double`, and `long double` may also be used with
runtime bounds.
```cpp
template <typename T,
T Min = T(),
T Max = T(),
bool RuntimeSpecialisation = ((Min == T()) && (Max == T()))>
// C++11 and later
template <typename T, T... Limits>
class clamped_value;
```
@ -39,8 +40,11 @@ etl::clamped_value<int, 2, 7> value_ct; // Fixed range [2, 7].
etl::clamped_value<int> value_rt(2, 7, 5); // Runtime range and initial value.
#if ETL_HAS_FLOATING_POINT_CLAMPED_VALUE
etl::clamped_value<double> float_rt(-1.5, 2.5, 0.5);
#endif
#if ETL_HAS_COMPILE_TIME_FLOATING_POINT_CLAMPED_VALUE
etl::clamped_value<float, -1.5f, 2.5f> float_ct(0.5f);
etl::clamped_value<double> float_rt(-1.5, 2.5, 0.5);
#endif
```
@ -67,13 +71,10 @@ clamped_value<int> value(min, max, initial);
Creates a runtime clamped value. The two-argument form initializes the value
to `min`; the three-argument form clamps `initial` to the range.
`min` must not be greater than `max`. As with `cyclic_value`, zero-initialized
`Min` and `Max` select the runtime-bound specialization; use a non-zero
compile-time bound when a fixed compile-time range is needed. NaN bounds and
initial values are rejected. Infinite bounds and values are permitted.
Because zero-initialized `Min` and `Max` select runtime bounds, a fixed
compile-time range `[0, 0]` cannot be represented by this dispatch scheme.
`min` must not be greater than `max`. Omitting the template bounds selects the
runtime-bound specialization. Supplying `Min` and `Max` selects fixed
compile-time bounds. NaN bounds and initial values are rejected. Infinite
bounds and values are permitted.
## Modifiers

View File

@ -43,21 +43,32 @@ SOFTWARE.
///\def ETL_HAS_FLOATING_POINT_CLAMPED_VALUE
/// Set to `1` when clamped_value supports floating-point types.
/// Floating-point support requires C++20 non-type template arguments.
/// Runtime floating-point support requires C++11 variadic templates.
/// The macro may be defined by the build or platform profile to override
/// automatic detection.
#if !defined(ETL_HAS_FLOATING_POINT_CLAMPED_VALUE)
#if ETL_USING_CPP20 && defined(__cpp_nontype_template_args) && (__cpp_nontype_template_args >= 201911L)
#if ETL_USING_CPP11
#define ETL_HAS_FLOATING_POINT_CLAMPED_VALUE 1
#else
#define ETL_HAS_FLOATING_POINT_CLAMPED_VALUE 0
#endif
#endif
///\def ETL_HAS_COMPILE_TIME_FLOATING_POINT_CLAMPED_VALUE
/// Set to `1` when clamped_value supports floating-point compile-time bounds.
#if !defined(ETL_HAS_COMPILE_TIME_FLOATING_POINT_CLAMPED_VALUE)
#if ETL_HAS_FLOATING_POINT_CLAMPED_VALUE && ETL_USING_CPP20 && defined(__cpp_nontype_template_args) && (__cpp_nontype_template_args >= 201911L)
#define ETL_HAS_COMPILE_TIME_FLOATING_POINT_CLAMPED_VALUE 1
#else
#define ETL_HAS_COMPILE_TIME_FLOATING_POINT_CLAMPED_VALUE 0
#endif
#endif
///\defgroup clamped_value clamped_value
/// Provides a value that is clamped between two limits.
/// Integral types are supported in all language modes. Floating-point types
/// are supported when ETL_HAS_FLOATING_POINT_CLAMPED_VALUE is `1`.
/// with runtime bounds are supported when
/// ETL_HAS_FLOATING_POINT_CLAMPED_VALUE is `1`.
/// \ingroup utilities
namespace etl
@ -113,18 +124,18 @@ namespace etl
return value;
}
const unsigned_type current = etl::to_unsigned(value); //static_cast<unsigned_type>(value);
const unsigned_type current = etl::to_unsigned(value); // static_cast<unsigned_type>(value);
const unsigned_type step = etl::absolute_unsigned(n);
if ((n > 0) && (value < max_value))
{
const unsigned_type distance = etl::to_unsigned(max_value) - current; //static_cast<unsigned_type>(max_value) - current;
const unsigned_type distance = etl::to_unsigned(max_value) - current; // static_cast<unsigned_type>(max_value) - current;
value = (step >= distance) ? max_value : static_cast<T>(current + step);
}
else if ((n < 0) && (value > min_value))
{
const unsigned_type distance = current - etl::to_unsigned(min_value); //static_cast<unsigned_type>(min_value);
const unsigned_type distance = current - etl::to_unsigned(min_value); // static_cast<unsigned_type>(min_value);
value = (step >= distance) ? min_value : static_cast<T>(current - step);
}
@ -236,22 +247,32 @@ namespace etl
} // namespace private_clamped_value
/// \endcond
#include "private/diagnostic_float_equal_push.h"
#if ETL_USING_CPP11
template <typename T, T... Limits>
class clamped_value;
#else // ETL_NOT_USING_CPP11
#include "private/diagnostic_float_equal_push.h"
template <typename T, T Min = T(), T Max = T(), bool RuntimeSpecialisation = ((Min == T()) && (Max == T()))>
class clamped_value;
#include "private/diagnostic_pop.h"
#include "private/diagnostic_pop.h"
#endif
//***************************************************************************
/// Provides a value that is clamped between two compile-time limits.
/// Supports incrementing, decrementing and arbitrary advance.
///@tparam T An integral type, or a floating-point type when
/// ETL_HAS_FLOATING_POINT_CLAMPED_VALUE is `1`.
/// ETL_HAS_COMPILE_TIME_FLOATING_POINT_CLAMPED_VALUE is `1`.
///@tparam Min The minimum value of the range.
///@tparam Max The maximum value of the range.
///\ingroup clamped_value
//***************************************************************************
#if ETL_USING_CPP11
template <typename T, T Min, T Max>
class clamped_value<T, Min, Max>
#else // ETL_NOT_USING_CPP11
template <typename T, T Min, T Max>
class clamped_value<T, Min, Max, false>
#endif
{
public:
@ -583,8 +604,13 @@ namespace etl
///@tparam Max Ignored for this specialisation.
///\ingroup clamped_value
//***************************************************************************
#if ETL_USING_CPP11
template <typename T>
class clamped_value<T>
#else // ETL_NOT_USING_CPP11
template <typename T, T Min, T Max>
class clamped_value<T, Min, Max, true>
#endif
{
public:

View File

@ -31,6 +31,9 @@ SOFTWARE.
#include <etl/clamped_value.h>
#if ETL_HAS_FLOATING_POINT_CLAMPED_VALUE
etl::clamped_value<float, -1.0f, 1.0f> clamped_float_compile_time;
etl::clamped_value<double> clamped_double_run_time;
etl::clamped_value<double> clamped_double_run_time;
#endif
#if ETL_HAS_COMPILE_TIME_FLOATING_POINT_CLAMPED_VALUE
etl::clamped_value<float, -1.0f, 1.0f> clamped_float_compile_time;
#endif

View File

@ -61,6 +61,17 @@ namespace
CHECK_EQUAL(7, value.max());
}
#if ETL_USING_CPP11
//*************************************************************************
TEST(test_compile_time_zero_range)
{
etl::clamped_value<int, 0, 0> value(1);
CHECK_EQUAL(0, value.get());
CHECK_EQUAL(0, value.min());
CHECK_EQUAL(0, value.max());
}
#endif
//*************************************************************************
TEST(test_run_time_initialisation)
{
@ -364,7 +375,7 @@ namespace
CHECK_THROW(value.set(7, 2), etl::exception);
}
#if ETL_HAS_FLOATING_POINT_CLAMPED_VALUE
#if ETL_HAS_COMPILE_TIME_FLOATING_POINT_CLAMPED_VALUE
//*************************************************************************
TEST(test_compile_time_floating_clamps_and_advances)
{
@ -381,7 +392,9 @@ namespace
value.advance(10.0f);
CHECK_CLOSE(2.5f, value.get(), 0.0001f);
}
#endif
#if ETL_HAS_FLOATING_POINT_CLAMPED_VALUE
//*************************************************************************
TEST(test_runtime_floating_clamps_and_advances)
{
@ -396,45 +409,52 @@ namespace
value.advance(-10.0);
CHECK_CLOSE(-1.5, value.get(), 0.0001);
}
#endif
#if ETL_HAS_COMPILE_TIME_FLOATING_POINT_CLAMPED_VALUE
//*************************************************************************
TEST(test_floating_increment_and_decrement_saturate_at_fractional_bounds)
TEST(test_compile_time_floating_increment_saturates_at_fractional_bound)
{
etl::clamped_value<float, -0.5f, 0.5f> compile_time(0.25f);
etl::clamped_value<float> run_time(-0.5f, 0.5f, -0.25f);
++compile_time;
--run_time;
CHECK_CLOSE(0.5f, compile_time.get(), 0.0001f);
CHECK_CLOSE(-0.5f, run_time.get(), 0.0001f);
}
#endif
#if ETL_HAS_FLOATING_POINT_CLAMPED_VALUE
//*************************************************************************
TEST(test_runtime_floating_decrement_saturates_at_fractional_bound)
{
etl::clamped_value<float> value(-0.5f, 0.5f, -0.25f);
--value;
CHECK_CLOSE(-0.5f, value.get(), 0.0001f);
}
//*************************************************************************
TEST(test_floating_comparison_and_swap)
TEST(test_runtime_floating_comparison_and_swap)
{
etl::clamped_value<double, -2.0, 2.0> compile_time1(-0.5);
etl::clamped_value<double, -2.0, 2.0> compile_time2(1.5);
etl::clamped_value<double> run_time1(-3.0, 3.0, -1.0);
etl::clamped_value<double> run_time2(4.0, 8.0, 6.0);
etl::clamped_value<double> value1(-3.0, 3.0, -1.0);
etl::clamped_value<double> value2(4.0, 8.0, 6.0);
CHECK(compile_time1 < compile_time2);
CHECK(compile_time1 < 1.0);
swap(compile_time1, compile_time2);
swap(run_time1, run_time2);
CHECK(value1 < value2);
CHECK(value1 < 1.0);
swap(value1, value2);
CHECK_CLOSE(1.5, compile_time1.get(), 0.0001);
CHECK_CLOSE(6.0, run_time1.get(), 0.0001);
CHECK_CLOSE(4.0, run_time1.min(), 0.0001);
CHECK_CLOSE(-1.0, run_time2.get(), 0.0001);
CHECK_CLOSE(6.0, value1.get(), 0.0001);
CHECK_CLOSE(4.0, value1.min(), 0.0001);
CHECK_CLOSE(-1.0, value2.get(), 0.0001);
}
//*************************************************************************
TEST(test_floating_infinity_saturates)
TEST(test_runtime_floating_infinity_saturates)
{
const double infinity = etl::numeric_limits<double>::infinity();
etl::clamped_value<double, -2.0, 2.0> value(0.0);
etl::clamped_value<double> infinite_range(-infinity, infinity, 0.0);
const double infinity = etl::numeric_limits<double>::infinity();
etl::clamped_value<double> value(-2.0, 2.0, 0.0);
etl::clamped_value<double> infinite_range(-infinity, infinity, 0.0);
value += infinity;
CHECK_CLOSE(2.0, value.get(), 0.0001);
@ -501,7 +521,7 @@ namespace
CHECK(true);
}
#if ETL_HAS_FLOATING_POINT_CLAMPED_VALUE && (ETL_USING_MICROSOFT_COMPILER && (_MSC_VER >= 1950))
#if ETL_HAS_COMPILE_TIME_FLOATING_POINT_CLAMPED_VALUE && (ETL_USING_MICROSOFT_COMPILER && (_MSC_VER >= 1950))
//*************************************************************************
TEST(test_floating_clamped_value_constexpr)
{