Skip to content

Commit fca5e6c

Browse files
reject out-of-range values in the double to float conversions (#1249)
Converting a finite double beyond the range of float (e.g. 1e300 or 1e39) was a bare static_cast: undefined behavior, in practice an infinity. It now throws, both in Any::cast<float>() / getInput<float>() / Blackboard::get<float>() and in the macOS / GCC <= 10 convertFromString<float> fallback, matching std::from_chars<float>. Values below FLT_MAX + half an ulp (e.g. 3.4028235e38, the usual printout of FLT_MAX) still round to FLT_MAX; NaN and infinities pass through unchanged. Behavior change: reading a finite double with |x| >= FLT_MAX + half an ulp as float now throws instead of returning an infinity.
1 parent 7f0576b commit fca5e6c

4 files changed

Lines changed: 84 additions & 8 deletions

File tree

‎include/behaviortree_cpp/utils/convert_impl.hpp‎

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,29 @@ inline void checkLowerLimit(const From& from)
8787
}
8888
}
8989

90+
// Rounding to a narrower floating point type is accepted, but a finite value
91+
// beyond the range of To has no representation there: the conversion is
92+
// undefined and in practice produces an infinity.
93+
template <typename From, typename To>
94+
inline void checkFloatingPointRange(const From& from)
95+
{
96+
if constexpr(std::numeric_limits<To>::max_exponent <
97+
std::numeric_limits<From>::max_exponent)
98+
{
99+
// Values below max + half an ulp still round to max (e.g. 3.4028235e38,
100+
// the usual printout of FLT_MAX); from there on they round to infinity.
101+
constexpr int half_ulp_exp =
102+
std::numeric_limits<To>::max_exponent - std::numeric_limits<To>::digits - 1;
103+
const From limit = static_cast<From>(std::numeric_limits<To>::max()) +
104+
std::ldexp(From(1), half_ulp_exp);
105+
if(std::isfinite(from) && std::fabs(from) >= limit)
106+
{
107+
throw std::runtime_error("Value outside the numeric limits of the floating point "
108+
"type");
109+
}
110+
}
111+
}
112+
90113
template <typename From, typename To>
91114
inline void checkTruncation(const From& from)
92115
{
@@ -132,6 +155,11 @@ inline void checkTruncation(const From& from)
132155
throw std::runtime_error("Invalid floating point to integer conversion");
133156
}
134157
}
158+
// Handle floating point to floating point (e.g. double to float)
159+
else if constexpr(std::is_floating_point_v<From> && std::is_floating_point_v<To>)
160+
{
161+
checkFloatingPointRange<From, To>(from);
162+
}
135163
// Handle other conversions
136164
else
137165
{
@@ -202,13 +230,7 @@ void convertNumber(const SRC& source, DST& target)
202230
else if constexpr(std::is_floating_point<SRC>::value ||
203231
std::is_floating_point<DST>::value)
204232
{
205-
bool both_float =
206-
std::is_floating_point<SRC>::value && std::is_floating_point<DST>::value;
207-
// to avoid being too pedantic, let's accept casting between double and float
208-
if(!both_float)
209-
{
210-
checkTruncation<SRC, DST>(source);
211-
}
233+
checkTruncation<SRC, DST>(source);
212234
target = static_cast<DST>(source);
213235
}
214236
}

‎include/behaviortree_cpp/utils/safe_any.hpp‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,10 @@ inline nonstd::expected<T, std::string> Any::stringToNumber() const
475475
double value = 0.0;
476476
if(parseDouble(str.toStdStringView(), value, /*require_full_consumption=*/false))
477477
{
478-
return static_cast<T>(value);
478+
// same range check as the integral branch: a value that does not fit
479+
// in a float is an error, like std::from_chars reports above
480+
Any temp_any(value);
481+
return temp_any.convert<T>();
479482
}
480483
return nonstd::make_unexpected("Any failed string to number conversion");
481484
}

‎src/basic_types.cpp‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#if !defined(__cpp_lib_to_chars) || (__cpp_lib_to_chars < 201611L)
1212
#include <cctype>
1313
#include <cerrno>
14+
#include <cmath>
15+
#include <limits>
1416

1517
#include <locale.h>
1618
#if defined(__APPLE__)
@@ -299,6 +301,13 @@ float convertFromString<float>(StringView str)
299301
{
300302
throw RuntimeError(StrCat("Can't convert string [", str, "] to float"));
301303
}
304+
// std::from_chars<float> reports result_out_of_range for a finite value that
305+
// does not fit in a float; casting the double would silently give an infinity.
306+
// 0x1.ffffffp+127 is FLT_MAX + half an ulp: values below it round to FLT_MAX.
307+
if(std::isfinite(result) && std::fabs(result) >= 0x1.ffffffp+127)
308+
{
309+
throw RuntimeError(StrCat("Can't convert string [", str, "] to float"));
310+
}
302311
return static_cast<float>(result);
303312
#endif
304313
}

‎tests/gtest_basic_types.cpp‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,48 @@ TEST(BasicTypes, IntToDoubleBoundary)
167167
EXPECT_EQ(bb->get<double>("d"), 1000.0);
168168
}
169169

170+
TEST(BasicTypes, FloatConversionLimits)
171+
{
172+
// A double beyond the float range has no float representation: the cast is
173+
// undefined and used to return an infinity. Reject it like every other
174+
// out-of-range narrowing.
175+
EXPECT_ANY_THROW((void)BT::Any(1e300).cast<float>());
176+
EXPECT_ANY_THROW((void)BT::Any(-1e300).cast<float>());
177+
178+
// Rounding to float is still accepted, as are values close to the limit.
179+
EXPECT_FLOAT_EQ(BT::Any(0.1).cast<float>(), 0.1f);
180+
EXPECT_FLOAT_EQ(BT::Any(3.4e38).cast<float>(), 3.4e38f);
181+
EXPECT_FLOAT_EQ(BT::Any(-3.4e38).cast<float>(), -3.4e38f);
182+
183+
// 3.4028235e38 (the usual printout of FLT_MAX) is a bit above FLT_MAX as a
184+
// double, but below FLT_MAX + half an ulp, so it rounds to FLT_MAX.
185+
constexpr float flt_max = std::numeric_limits<float>::max();
186+
EXPECT_EQ(BT::Any(3.4028235e38).cast<float>(), flt_max);
187+
EXPECT_EQ(BT::Any(-3.4028235e38).cast<float>(), -flt_max);
188+
EXPECT_EQ(convertFromString<float>("3.4028235e38"), flt_max);
189+
// FLT_MAX + half an ulp rounds to infinity.
190+
EXPECT_ANY_THROW((void)BT::Any(0x1.ffffffp+127).cast<float>());
191+
EXPECT_ANY_THROW((void)convertFromString<float>("6.8056473e38"));
192+
// Widening never throws, and non-finite values pass through.
193+
EXPECT_EQ(BT::Any(flt_max).cast<double>(), double(flt_max));
194+
EXPECT_TRUE(std::isinf(BT::Any(std::numeric_limits<double>::infinity()).cast<float>()));
195+
196+
// Same path through a blackboard entry holding a double.
197+
auto bb = BT::Blackboard::create();
198+
bb->set("v", 1e300);
199+
EXPECT_ANY_THROW((void)bb->get<float>("v"));
200+
bb->set("v", 2.5);
201+
EXPECT_FLOAT_EQ(bb->get<float>("v"), 2.5f);
202+
203+
// The string conversions must agree with std::from_chars<float>, which
204+
// reports result_out_of_range for these literals.
205+
EXPECT_ANY_THROW((void)convertFromString<float>("1e300"));
206+
EXPECT_ANY_THROW((void)convertFromString<float>("-1e300"));
207+
EXPECT_FLOAT_EQ(convertFromString<float>("1.5"), 1.5f);
208+
EXPECT_ANY_THROW((void)BT::Any(std::string("1e300")).cast<float>());
209+
EXPECT_FLOAT_EQ(BT::Any(std::string("1.5")).cast<float>(), 1.5f);
210+
}
211+
170212
TEST(BasicTypes, ConvertFromString_Bool)
171213
{
172214
ASSERT_TRUE(convertFromString<bool>("true"));

0 commit comments

Comments
 (0)