Skip to content

Commit

Permalink
Dropped dependency on Boost.NumericConversion and Boost.MPL (#72)
Browse files Browse the repository at this point in the history
Fix some corner cases of floating-point conversions (infinities to infinities of smaller type, fractional numbers to integers)
  • Loading branch information
apolukhin committed Feb 13, 2024
1 parent fc5ffb6 commit 518e28f
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 80 deletions.
1 change: 0 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ target_link_libraries(boost_lexical_cast
Boost::container
Boost::core
Boost::integer
Boost::numeric_conversion
Boost::throw_exception
Boost::type_traits
)
Expand Down
1 change: 1 addition & 0 deletions doc/lexical_cast.qbk
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ limitation of compiler options that you use.
* less template instantiations and simpler maintainance;
* support for `volatile` input types was dropped, following the C++ Standard Library trend.
* Optimized conversions from std::basic_string_view and boost::basic_string_view
* Dropped dependency on Boost.NumericConversion and Boost.MPL

* [*boost 1.84.0 :]

Expand Down
153 changes: 77 additions & 76 deletions include/boost/lexical_cast/detail/converter_numeric.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,101 +23,104 @@
# pragma once
#endif

#include <boost/core/cmath.hpp>
#include <boost/core/enable_if.hpp>
#include <boost/limits.hpp>
#include <boost/type_traits/type_identity.hpp>
#include <boost/type_traits/conditional.hpp>
#include <boost/type_traits/make_unsigned.hpp>
#include <boost/type_traits/is_signed.hpp>
#include <boost/type_traits/is_integral.hpp>
#include <boost/type_traits/is_arithmetic.hpp>
#include <boost/type_traits/is_base_of.hpp>
#include <boost/type_traits/is_float.hpp>
#include <boost/type_traits/remove_volatile.hpp>

#include <boost/numeric/conversion/cast.hpp>

namespace boost { namespace detail {

template <class Source >
struct detect_precision_loss
{
typedef Source source_type;
typedef boost::numeric::Trunc<Source> Rounder;
typedef typename conditional<
boost::is_arithmetic<Source>::value, Source, Source const&
>::type argument_type ;

static inline source_type nearbyint(argument_type s, bool& is_ok) noexcept {
const source_type near_int = Rounder::nearbyint(s);
if (near_int && is_ok) {
const source_type orig_div_round = s / near_int;
const source_type eps = std::numeric_limits<source_type>::epsilon();

is_ok = !((orig_div_round > 1 ? orig_div_round - 1 : 1 - orig_div_round) > eps);
}
template <class Source, class Target>
bool ios_numeric_comparer_float(Source x, Source y) noexcept {
return x == y
|| (boost::core::isnan(x) && boost::core::isnan(y))
|| (x < (std::numeric_limits<Target>::min)())
;
}

template <class RangeType, class T>
constexpr bool is_out_of_range_for(T value) noexcept {
return value > static_cast<T>((std::numeric_limits<RangeType>::max)())
|| value < static_cast<T>((std::numeric_limits<RangeType>::min)());
}

return s;

// integral -> integral
template <typename Target, typename Source>
typename boost::enable_if_c<
!boost::is_floating_point<Source>::value && !boost::is_floating_point<Target>::value, bool
>::type noexcept_numeric_convert(Source arg, Target& result) noexcept {
const Target target_tmp = static_cast<Target>(arg);
const Source arg_restored = static_cast<Source>(target_tmp);
if (arg == arg_restored) {
result = target_tmp;
return true;
}
return false;
}

typedef typename Rounder::round_style round_style;
};
// integral -> floating point
template <typename Target, typename Source>
typename boost::enable_if_c<
!boost::is_floating_point<Source>::value && boost::is_floating_point<Target>::value, bool
>::type noexcept_numeric_convert(Source arg, Target& result) noexcept {
const Target target_tmp = static_cast<Target>(arg);
result = target_tmp;
return true;
}

template <typename Base, class Source>
struct fake_precision_loss: public Base
{
typedef Source source_type ;
typedef typename conditional<
boost::is_arithmetic<Source>::value, Source, Source const&
>::type argument_type ;

static inline source_type nearbyint(argument_type s, bool& /*is_ok*/) noexcept {
return s;
// floating point -> floating point
template <typename Target, typename Source>
typename boost::enable_if_c<
boost::is_floating_point<Source>::value && boost::is_floating_point<Target>::value, bool
>::type noexcept_numeric_convert(Source arg, Target& result) noexcept {
const Target target_tmp = static_cast<Target>(arg);
const Source arg_restored = static_cast<Source>(target_tmp);
if (detail::ios_numeric_comparer_float<Source, Target>(arg, arg_restored)) {
result = target_tmp;
return true;
}
};

struct nothrow_overflow_handler
{
inline bool operator() ( boost::numeric::range_check_result r ) const noexcept {
return (r == boost::numeric::cInRange);
}
};
return false;
}

// floating point -> integral
template <typename Target, typename Source>
inline bool noexcept_numeric_convert(const Source& arg, Target& result) noexcept {
typedef boost::numeric::converter<
Target,
Source,
boost::numeric::conversion_traits<Target, Source >,
nothrow_overflow_handler,
detect_precision_loss<Source >
> converter_orig_t;

typedef typename boost::conditional<
boost::is_base_of< detect_precision_loss<Source >, converter_orig_t >::value,
converter_orig_t,
fake_precision_loss<converter_orig_t, Source>
>::type converter_t;

bool res = nothrow_overflow_handler()(converter_t::out_of_range(arg));
if (res) {
result = converter_t::low_level_convert(converter_t::nearbyint(arg, res));
typename boost::enable_if_c<
boost::is_floating_point<Source>::value && !boost::is_floating_point<Target>::value, bool
>::type noexcept_numeric_convert(Source arg, Target& result) noexcept {
if (detail::is_out_of_range_for<Target>(arg)) {
return false;
}

const Target target_tmp = static_cast<Target>(arg);
const Source arg_restored = static_cast<Source>(target_tmp);
if (detail::ios_numeric_comparer_float<Source, Target>(arg, arg_restored)) {
result = target_tmp;
return true;
}

return res;
return false;
}

template <typename Target, typename Source>
struct lexical_cast_dynamic_num_not_ignoring_minus
{
static inline bool try_convert(const Source &arg, Target& result) noexcept {
return noexcept_numeric_convert<Target, Source >(arg, result);
template <typename Target, typename Source>
static inline bool try_convert(Source arg, Target& result) noexcept {
return boost::detail::noexcept_numeric_convert<Target, Source >(arg, result);
}
};

template <typename Target, typename Source>
struct lexical_cast_dynamic_num_ignoring_minus
{
static inline bool try_convert(const Source &arg, Target& result) noexcept {
template <typename Target, typename Source>
static inline bool try_convert(Source arg, Target& result) noexcept {
typedef typename boost::conditional<
boost::is_float<Source>::value,
boost::type_identity<Source>,
Expand All @@ -126,17 +129,17 @@ struct lexical_cast_dynamic_num_ignoring_minus
typedef typename usource_lazy_t::type usource_t;

if (arg < 0) {
const bool res = noexcept_numeric_convert<Target, usource_t>(0u - arg, result);
result = static_cast<Target>(0u - result);
const bool res = boost::detail::noexcept_numeric_convert<Target, usource_t>(0u - arg, result);
result = static_cast<Target>(0u) - static_cast<Target>(result);
return res;
} else {
return noexcept_numeric_convert<Target, usource_t>(arg, result);
return boost::detail::noexcept_numeric_convert<Target, usource_t>(arg, result);
}
}
};

/*
* lexical_cast_dynamic_num follows the rules:
* dynamic_num_converter_impl follows the rules:
* 1) If Source can be converted to Target without precision loss and
* without overflows, then assign Source to Target and return
*
Expand All @@ -156,16 +159,14 @@ struct lexical_cast_dynamic_num_ignoring_minus
template <typename Target, typename Source>
struct dynamic_num_converter_impl
{
typedef typename boost::remove_volatile<Source>::type source_type;

static inline bool try_convert(source_type arg, Target& result) noexcept {
static inline bool try_convert(Source arg, Target& result) noexcept {
typedef typename boost::conditional<
boost::is_unsigned<Target>::value &&
(boost::is_signed<source_type>::value || boost::is_float<source_type>::value) &&
!(boost::is_same<source_type, bool>::value) &&
(boost::is_signed<Source>::value || boost::is_float<Source>::value) &&
!(boost::is_same<Source, bool>::value) &&
!(boost::is_same<Target, bool>::value),
lexical_cast_dynamic_num_ignoring_minus<Target, source_type>,
lexical_cast_dynamic_num_not_ignoring_minus<Target, source_type>
lexical_cast_dynamic_num_ignoring_minus,
lexical_cast_dynamic_num_not_ignoring_minus
>::type caster_type;

return caster_type::try_convert(arg, result);
Expand Down
8 changes: 7 additions & 1 deletion test/Jamfile.v2
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ test-suite conversion
[ run implicit_convert.cpp ]
[ run wchars_test.cpp ]
[ run float_types_test.cpp ]

# Make sure that LexicalCast works the same way as some of the C++ Standard Libraries
[ run float_types_test.cpp : : : <define>BOOST_LEXICAL_CAST_DETAIL_TEST_ON_OLD
<toolset>msvc:<build>no # could have outdated behavior in some border cases
: float_types_non_opt
]

[ run inf_nan_test.cpp ]
[ run containers_test.cpp : : : <toolset>gcc:<cxxflags>-Wno-long-long <toolset>clang:<cxxflags>-Wno-long-long ]
[ run empty_input_test.cpp ]
Expand All @@ -55,7 +62,6 @@ test-suite conversion
[ run no_exceptions_test.cpp : : : <exception-handling>off
<define>_HAS_EXCEPTIONS=0 # MSVC stdlib
<define>_STLP_NO_EXCEPTIONS # STLPort
-<library>/boost/test//boost_unit_test_framework # uses lightweight_test
]
[ run iterator_range_test.cpp ]
[ run string_view_test.cpp ]
Expand Down
81 changes: 80 additions & 1 deletion test/float_types_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,23 @@
// Software License, Version 1.0. (See accompanying file
// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt).

#ifndef BOOST_LEXICAL_CAST_DETAIL_TEST_ON_OLD
#include <boost/lexical_cast.hpp>
#else
// Make sure that tests work the same way on non-optimized version
#include "lexical_cast_old.hpp"
#endif

#include <boost/cstdint.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/type_traits/is_same.hpp>
#include <boost/type_traits/is_signed.hpp>

#ifndef BOOST_TEST_CLOSE_FRACTION
// Naiive, but works for most tests in this file
#define BOOST_TEST_CLOSE_FRACTION(x, y, eps) BOOST_TEST(x - y + eps <= eps * 2)
#define BOOST_TEST_CLOSE_FRACTION(x, y, eps) \
BOOST_TEST(x - y + eps <= eps * 2); \
BOOST_TEST(y - x + eps <= eps * 2);
#endif

#if (defined(__CYGWIN__) || defined(__FreeBSD__) || defined(__NetBSD__) \
Expand Down Expand Up @@ -303,6 +312,13 @@ void test_float_typess_for_overflows()
&& lexical_cast<test_t>( (std::numeric_limits<double>::min)() )
<= (std::numeric_limits<double>::min)() + std::numeric_limits<test_t>::epsilon()
);

BOOST_TEST(
(std::numeric_limits<double>::min)() / 2 - std::numeric_limits<test_t>::epsilon()
<= lexical_cast<test_t>( (std::numeric_limits<double>::min)() / 2 )
&& lexical_cast<test_t>( (std::numeric_limits<double>::min)() / 2 )
<= (std::numeric_limits<double>::min)() / 2 + std::numeric_limits<test_t>::epsilon()
);
}

if ( sizeof(test_t) < sizeof(long double) )
Expand All @@ -314,6 +330,13 @@ void test_float_typess_for_overflows()
&& lexical_cast<test_t>( (std::numeric_limits<long double>::min)() )
<= (std::numeric_limits<long double>::min)() + std::numeric_limits<test_t>::epsilon()
);

BOOST_TEST(
(std::numeric_limits<long double>::min)() / 2 - std::numeric_limits<test_t>::epsilon()
<= lexical_cast<test_t>( (std::numeric_limits<long double>::min)() / 2 )
&& lexical_cast<test_t>( (std::numeric_limits<long double>::min)() / 2 )
<= (std::numeric_limits<long double>::min)() / 2 + std::numeric_limits<test_t>::epsilon()
);
}
}

Expand Down Expand Up @@ -488,6 +511,7 @@ void test_conversion_from_to_float()
}



void test_conversion_from_to_float()
{
test_conversion_from_to_float<float>();
Expand All @@ -505,11 +529,66 @@ void test_conversion_from_to_long_double()
BOOST_TEST(true);
}

template <class Integral, class Float>
void test_conversion_integral_float()
{
BOOST_TEST_CLOSE_FRACTION(lexical_cast<Float>(static_cast<Float>(1)), static_cast<Float>(1), std::numeric_limits<Float>::epsilon());
BOOST_TEST_CLOSE_FRACTION(lexical_cast<Float>(static_cast<Float>(1.1234)), static_cast<Float>(1.1234), std::numeric_limits<Float>::epsilon());
BOOST_TEST_CLOSE_FRACTION(lexical_cast<Float>(static_cast<Float>(-1.1234)), static_cast<Float>(-1.1234), std::numeric_limits<Float>::epsilon());

BOOST_TEST_CLOSE_FRACTION(lexical_cast<Float>(static_cast<Integral>(0)), static_cast<Float>(0), std::numeric_limits<Float>::epsilon());
BOOST_TEST_CLOSE_FRACTION(lexical_cast<Float>(static_cast<Integral>(1)), static_cast<Float>(1), std::numeric_limits<Float>::epsilon());

#ifndef __CYGWIN__
BOOST_TEST_CLOSE_FRACTION(lexical_cast<Float>((std::numeric_limits<Integral>::max)()), static_cast<Float>((std::numeric_limits<Integral>::max)()), std::numeric_limits<Float>::epsilon());
BOOST_TEST_CLOSE_FRACTION(lexical_cast<Float>((std::numeric_limits<Integral>::min)()), static_cast<Float>((std::numeric_limits<Integral>::min)()), std::numeric_limits<Float>::epsilon());
#endif

BOOST_TEST_EQ(lexical_cast<Integral>(static_cast<Float>(0.0)), 0);
BOOST_TEST_EQ(lexical_cast<Integral>(static_cast<Float>(1.0)), 1);
BOOST_TEST_EQ(lexical_cast<Integral>(static_cast<Float>(8.0)), 8);
BOOST_TEST_EQ(lexical_cast<Integral>(static_cast<Float>(16.0)), 16);

if (boost::is_signed<Integral>::value) {
BOOST_TEST_EQ(lexical_cast<Integral>(static_cast<Float>(-1.0)), -1);
BOOST_TEST_EQ(lexical_cast<Integral>(static_cast<Float>(-8.0)), -8);
BOOST_TEST_EQ(lexical_cast<Integral>(static_cast<Float>(-16.0)), -16);
} else {
BOOST_TEST_EQ(lexical_cast<Integral>(static_cast<Float>(-1.0)), (std::numeric_limits<Integral>::max)());
BOOST_TEST_EQ(lexical_cast<Integral>(static_cast<Float>(-8.0)), (std::numeric_limits<Integral>::max)() - 7);
BOOST_TEST_EQ(lexical_cast<Integral>(static_cast<Float>(-16.0)), (std::numeric_limits<Integral>::max)() - 15);
}

BOOST_TEST_THROWS(lexical_cast<Integral>(static_cast<Float>(0.5)), bad_lexical_cast);
BOOST_TEST_THROWS(lexical_cast<Integral>(static_cast<Float>(-0.5)), bad_lexical_cast);
BOOST_TEST_THROWS(lexical_cast<Integral>(static_cast<Float>(1.5)), bad_lexical_cast);
BOOST_TEST_THROWS(lexical_cast<Integral>(static_cast<Float>(-1.5)), bad_lexical_cast);

BOOST_TEST_THROWS(lexical_cast<Integral>((std::numeric_limits<Float>::min)()), bad_lexical_cast);
BOOST_TEST_THROWS(lexical_cast<Integral>((std::numeric_limits<Float>::max)()), bad_lexical_cast);
BOOST_TEST_THROWS(lexical_cast<Integral>((std::numeric_limits<Float>::epsilon)()), bad_lexical_cast);
BOOST_TEST_THROWS(lexical_cast<Integral>((std::numeric_limits<Float>::lowest)()), bad_lexical_cast);
}


int main()
{
test_conversion_from_to_float();
test_conversion_from_to_double();
test_conversion_from_to_long_double();

test_conversion_integral_float<int, float>();
test_conversion_integral_float<int, double>();
test_conversion_integral_float<unsigned short, float>();
test_conversion_integral_float<unsigned short, double>();
test_conversion_integral_float<short, float>();
test_conversion_integral_float<short, double>();
test_conversion_integral_float<long int, float>();
test_conversion_integral_float<long int, double>();
test_conversion_integral_float<long long, float>();
test_conversion_integral_float<long long, double>();
test_conversion_integral_float<unsigned long long, float>();
test_conversion_integral_float<unsigned long long, double>();

return boost::report_errors();
}
Loading

0 comments on commit 518e28f

Please sign in to comment.