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

View File

@ -43,21 +43,32 @@ SOFTWARE.
///\def ETL_HAS_FLOATING_POINT_CLAMPED_VALUE ///\def ETL_HAS_FLOATING_POINT_CLAMPED_VALUE
/// Set to `1` when clamped_value supports floating-point types. /// 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 /// The macro may be defined by the build or platform profile to override
/// automatic detection. /// automatic detection.
#if !defined(ETL_HAS_FLOATING_POINT_CLAMPED_VALUE) #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 #define ETL_HAS_FLOATING_POINT_CLAMPED_VALUE 1
#else #else
#define ETL_HAS_FLOATING_POINT_CLAMPED_VALUE 0 #define ETL_HAS_FLOATING_POINT_CLAMPED_VALUE 0
#endif #endif
#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 ///\defgroup clamped_value clamped_value
/// Provides a value that is clamped between two limits. /// Provides a value that is clamped between two limits.
/// Integral types are supported in all language modes. Floating-point types /// 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 /// \ingroup utilities
namespace etl namespace etl
@ -113,18 +124,18 @@ namespace etl
return value; 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); const unsigned_type step = etl::absolute_unsigned(n);
if ((n > 0) && (value < max_value)) 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); value = (step >= distance) ? max_value : static_cast<T>(current + step);
} }
else if ((n < 0) && (value > min_value)) 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); value = (step >= distance) ? min_value : static_cast<T>(current - step);
} }
@ -236,22 +247,32 @@ namespace etl
} // namespace private_clamped_value } // namespace private_clamped_value
/// \endcond /// \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()))> template <typename T, T Min = T(), T Max = T(), bool RuntimeSpecialisation = ((Min == T()) && (Max == T()))>
class clamped_value; 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. /// Provides a value that is clamped between two compile-time limits.
/// Supports incrementing, decrementing and arbitrary advance. /// Supports incrementing, decrementing and arbitrary advance.
///@tparam T An integral type, or a floating-point type when ///@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 Min The minimum value of the range.
///@tparam Max The maximum value of the range. ///@tparam Max The maximum value of the range.
///\ingroup clamped_value ///\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> template <typename T, T Min, T Max>
class clamped_value<T, Min, Max, false> class clamped_value<T, Min, Max, false>
#endif
{ {
public: public:
@ -583,8 +604,13 @@ namespace etl
///@tparam Max Ignored for this specialisation. ///@tparam Max Ignored for this specialisation.
///\ingroup clamped_value ///\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> template <typename T, T Min, T Max>
class clamped_value<T, Min, Max, true> class clamped_value<T, Min, Max, true>
#endif
{ {
public: public:

View File

@ -31,6 +31,9 @@ SOFTWARE.
#include <etl/clamped_value.h> #include <etl/clamped_value.h>
#if ETL_HAS_FLOATING_POINT_CLAMPED_VALUE #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 #endif

View File

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