Skip to content
Merged
106 changes: 79 additions & 27 deletions doc/modules/ROOT/pages/cfenv.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ https://www.boost.org/LICENSE_1_0.txt
= `<cfenv>` support
:idprefix: cfenv_

WARNING: This is an expert feature.
If you have never used `std::fegetround` nor `std::fesetround` before, it is unlikely you will need anything described on this page

== <cfenv>

IEEE 754 defines 5 rounding modes for Decimal Floating Point Types.
Expand All @@ -18,7 +21,7 @@ IEEE 754 defines 5 rounding modes for Decimal Floating Point Types.
4. Toward zero
5. Upward

NOTE: The default rounding mode is to nearest with ties to even (#2) as specified in IEEE 754 Section 4.3.3
NOTE: The default rounding mode is to nearest with ties to even (#2) as specified in IEEE 754 Section 4.3.3. This is also colloquially known as "Banker's Rounding"

Using the following `enum class` and functions you can change the rounding mode from the default at *RUNTIME* only if xref:config.adoc#configuration_automatic[`BOOST_DECIMAL_NO_CONSTEVAL_DETECTION`] is not defined.

Expand All @@ -31,11 +34,11 @@ namespace decimal {

enum class rounding_mode : unsigned
{
fe_dec_downward = 1 << 0,
fe_dec_to_nearest = 1 << 1,
fe_dec_to_nearest_from_zero = 1 << 2,
fe_dec_toward_zero = 1 << 3,
fe_dec_upward = 1 << 4,
fe_dec_downward,
fe_dec_to_nearest,
fe_dec_to_nearest_from_zero,
fe_dec_toward_zero,
fe_dec_upward,
fe_dec_default = fe_dec_to_nearest
};

Expand All @@ -52,35 +55,84 @@ rounding_mode fesetround(rounding_mode round) noexcept;
} //namespace boost
----

You can similarly change the default rounding mode at compile time with any compiler (unlike at runtime) using similarly named macros:
IMPORTANT: Much like `std::fesetround`, `boost::decimal::fesetround` is not thread safe.

Below is an example of the effects of changing the runtime rounding mode if your compiler supports it:

[source, c++]
----
#include <boost/decimal.hpp>
#include <cassert>

namespace boost {
namespace decimal {
int main()
{
using namespace boost::decimal::literals;

static constexpr rounding_mode _boost_decimal_global_rounding_mode {
#if defined(BOOST_DECIMAL_FE_DEC_DOWNWARD)
rounding_mode::fe_dec_downward
#elif defined(BOOST_DECIMAL_FE_DEC_TO_NEAREST)
rounding_mode::fe_dec_to_nearest
#elif defined(BOOST_DECIMAL_FE_DEC_TO_NEAREST_FROM_ZERO)
rounding_mode::fe_dec_to_nearest_from_zero
#elif defined (BOOST_DECIMAL_FE_DEC_TOWARD_ZERO)
rounding_mde::fe_dec_toward_zero
#elif defined(BOOST_DECIMAL_FE_DEC_UPWARD)
rounding_mode::fe_dec_upward
#else
rounding_mode::fe_dec_default
#endif
};
auto default_rounding_mode = boost::decimal::fegetround(); // Default is fe_dec_to_nearest

auto new_rounding_mode = boost::decimal::fesetround(boost::decimal::rounding_mode::fe_dec_upward);

} // namespace decimal
} // namespace boost
assert(default_rounding_mode != new_rounding_mode);

const auto lhs {"5e+50"_DF};
const auto rhs {"4e+40"_DF};

// With upward rounding the result will be "5.000001e+50"_DF
// Even though the difference in order of magnitude is greater than the precision of the type,
// any addition in this mode will result in at least a one ULP difference
const auto upward_res {lhs + rhs};
assert(upward_res == "5.000001e+50"_DF);

boost::decimal::fesetround(boost::decimal::rounding_mode::fe_dec_downward);

// Similar to above in the downward rounding mode any subtraction will result in at least a one ULP difference
const auto downward_res {lhs - rhs};
assert(downward_res == "4.999999e+50"_DF);

return 0;
}
----

The default rounding mode for compile time is the same as the default runtime.
As shown, changing the rounding mode *WILL* change your numerical results.
If you are coming from the Intel library (or other C-style libs) where every mathematical function takes a rounding mode, that is not the case in this library; the only way to change the rounding mode for individual operations is via the global rounding mode.
Before attempting to change the rounding mode ensure this is actually what you want to happen.

You can similarly change the default rounding mode at compile time with *ANY* compiler (unlike at runtime) using similarly named macros:

1. BOOST_DECIMAL_FE_DEC_DOWNWARD
2. BOOST_DECIMAL_FE_DEC_TO_NEAREST
3. BOOST_DECIMAL_FE_DEC_TO_NEAREST_FROM_ZERO
4. BOOST_DECIMAL_FE_DEC_TOWARD_ZERO
5. BOOST_DECIMAL_FE_DEC_UPWARD

If none of the above macros are defined, the default rounding mode for compile time is the same as the default runtime (i.e. as if BOOST_DECIMAL_FE_DEC_TO_NEAREST were defined).
At most *ONE* of these macros are allowed to be user defined.
A `#error` will be emitted if more than one is detected.
The macro must be defined before the inclusion of any decimal library header.
The rounding mode set at compile time is thread-safe as it is read only.

This same example can be reduced for the cases where:

1. Compiler does not support runtime rounding mode changes

2. You want to change the compile time rounding mode

[source, c++]
----
#define BOOST_DECIMAL_FE_DEC_DOWNWARD
#include <boost/decimal.hpp>

int main()
{
using namespace boost::decimal::literals;

constexpr auto lhs {"5e+50"_DF};
constexpr auto rhs {"4e+40"_DF};
constexpr auto downward_res {lhs - rhs};
static_assert(downward_res == "4.999999e+50"_DF, "Incorrectly rounded result");

return 0;
}
----

IMPORTANT: Prior to v5.2.0 this header was `<boost/decimal/fenv.hpp>`, but has been changed to `<boost/decimal/cfenv.hpp>` for consistency with the STL naming convention.
19 changes: 0 additions & 19 deletions doc/modules/ROOT/pages/examples.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -101,25 +101,6 @@ Output:
Returned Value: 0.25
----

[#examples_rounding_mode]
== Rounding Mode
[source, c++]
----
#include <boost/decimal.hpp>
#include <cassert>

int main()
{
auto default_rounding_mode = boost::decimal::fegetround(); // Default is fe_dec_to_nearest_from_zero

auto new_rounding_mode = boost::decimal::fesetround(boost::decimal::rounding_mode::fe_dec_to_nearest);

assert(default_rounding_mode != new_rounding_mode);

return 0;
}
----

[#examples_generic_programming]
== Generic Programming
[source, c++]
Expand Down
23 changes: 22 additions & 1 deletion examples/rounding_mode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,33 @@

int main()
{
#ifndef BOOST_DECIMAL_NO_CONSTEVAL_DETECTION

using namespace boost::decimal::literals;

BOOST_DECIMAL_ATTRIBUTE_UNUSED auto default_rounding_mode = boost::decimal::fegetround(); // Default is fe_dec_to_nearest

BOOST_DECIMAL_ATTRIBUTE_UNUSED auto new_rounding_mode = boost::decimal::fesetround(boost::decimal::rounding_mode::fe_dec_to_nearest_from_zero);
BOOST_DECIMAL_ATTRIBUTE_UNUSED auto new_rounding_mode = boost::decimal::fesetround(boost::decimal::rounding_mode::fe_dec_upward);

assert(default_rounding_mode != new_rounding_mode);

const auto lhs {"5e+50"_DF};
const auto rhs {"4e+40"_DF};

// With upward rounding the result will be "5.000001e+50"_DF
// Even though the difference in order of magnitude is greater than the precision of the type,
// any addition in this mode will result in at least a one ULP difference
BOOST_DECIMAL_ATTRIBUTE_UNUSED const auto upward_res {lhs + rhs};
assert(upward_res == "5.000001e+50"_DF);

boost::decimal::fesetround(boost::decimal::rounding_mode::fe_dec_downward);

// Similar to above in the downward rounding mode any subtraction will result in at least a one ULP difference
BOOST_DECIMAL_ATTRIBUTE_UNUSED const auto downward_res {lhs - rhs};
assert(downward_res == "4.999999e+50"_DF);

#endif // BOOST_DECIMAL_NO_CONSTEVAL_DETECTION

return 0;
}

18 changes: 18 additions & 0 deletions examples/rounding_mode_compile_time.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2024 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#define BOOST_DECIMAL_FE_DEC_DOWNWARD
#include <boost/decimal.hpp>

int main()
{
using namespace boost::decimal::literals;

constexpr auto lhs {"5e+50"_DF};
constexpr auto rhs {"4e+40"_DF};
constexpr auto downward_res {lhs - rhs};
static_assert(downward_res == "4.999999e+50"_DF, "Incorrectly rounded result");

return 0;
}
42 changes: 41 additions & 1 deletion include/boost/decimal/cfenv.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,46 @@

#include <boost/decimal/detail/config.hpp>

#ifdef BOOST_DECIMAL_FE_DEC_DOWNWARD
# define BOOST_DECIMAL_MODE_1 1
#else
# define BOOST_DECIMAL_MODE_1 0
#endif

#ifdef BOOST_DECIMAL_FE_DEC_TO_NEAREST
# define BOOST_DECIMAL_MODE_2 1
#else
# define BOOST_DECIMAL_MODE_2 0
#endif

#ifdef BOOST_DECIMAL_FE_DEC_TO_NEAREST_FROM_ZERO
# define BOOST_DECIMAL_MODE_3 1
#else
# define BOOST_DECIMAL_MODE_3 0
#endif

#ifdef BOOST_DECIMAL_FE_DEC_TOWARD_ZERO
# define BOOST_DECIMAL_MODE_4 1
#else
# define BOOST_DECIMAL_MODE_4 0
#endif

#ifdef BOOST_DECIMAL_FE_DEC_UPWARD
# define BOOST_DECIMAL_MODE_5 1
#else
# define BOOST_DECIMAL_MODE_5 0
#endif

// Now we can safely use arithmetic in preprocessor
#define BOOST_DECIMAL_MODE_COUNT \
(BOOST_DECIMAL_MODE_1 + BOOST_DECIMAL_MODE_2 + \
BOOST_DECIMAL_MODE_3 + BOOST_DECIMAL_MODE_4 + \
BOOST_DECIMAL_MODE_5)

#if BOOST_DECIMAL_MODE_COUNT > 1
# error "Only one rounding mode macro can be defined"
#endif

#ifndef BOOST_DECIMAL_BUILD_MODULE
#include <cfenv>
#endif
Expand Down Expand Up @@ -57,7 +97,7 @@ BOOST_DECIMAL_EXPORT inline auto fesetround(BOOST_DECIMAL_ATTRIBUTE_UNUSED const
_boost_decimal_global_runtime_rounding_mode = round;
#endif

return round;
return _boost_decimal_global_runtime_rounding_mode;
}

} // namespace decimal
Expand Down
12 changes: 12 additions & 0 deletions include/boost/decimal/detail/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,18 @@ typedef unsigned __int128 builtin_uint128_t;
# endif
#endif

// Clang < 12 will detect availability, but has issues
#ifdef BOOST_DECIMAL_HAS_BUILTIN_IS_CONSTANT_EVALUATED
# if defined(__clang__) && __clang_major__ < 12
# ifdef BOOST_DECIMAL_HAS_BUILTIN_IS_CONSTANT_EVALUATED
# undef BOOST_DECIMAL_HAS_BUILTIN_IS_CONSTANT_EVALUATED
# endif
# ifdef BOOST_DECIMAL_HAS_IS_CONSTANT_EVALUATED
# undef BOOST_DECIMAL_HAS_IS_CONSTANT_EVALUATED
# endif
# endif
#endif

//
// MSVC also supports __builtin_is_constant_evaluated if it's recent enough:
//
Expand Down
2 changes: 2 additions & 0 deletions test/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ run github_issue_1055.cpp ;
run github_issue_1057.cpp ;
compile-fail github_issue_1087.cpp ;
run github_issue_1091.cpp ;
run github_issue_1094.cpp ;
run link_1.cpp link_2.cpp link_3.cpp ;
run quick.cpp ;
run random_decimal32_comp.cpp ;
Expand Down Expand Up @@ -181,6 +182,7 @@ run ../examples/bit_conversions.cpp ;
run ../examples/charconv.cpp ;
run ../examples/literals.cpp ;
run ../examples/rounding_mode.cpp ;
run ../examples/rounding_mode_compile_time.cpp ;
run ../examples/moving_average.cpp ;
run ../examples/currency_conversion.cpp ;
run ../examples/statistics.cpp ;
Expand Down
4 changes: 4 additions & 0 deletions test/github_issue_1026.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,14 @@ int main()

BOOST_TEST_EQ("1"_DF / "5.24289e-96"_DF, "1.907345e+95"_DF);

#ifndef BOOST_DECIMAL_NO_CONSTEVAL_DETECTION

const auto new_rounding_mode = fesetround(rounding_mode::fe_dec_to_nearest_from_zero);
BOOST_TEST(new_rounding_mode == rounding_mode::fe_dec_to_nearest_from_zero);
BOOST_TEST_EQ(decimal32_t(100000, -105), "1e-100"_df);

#endif // BOOST_DECIMAL_NO_CONSTEVAL_DETECTION

return boost::report_errors();
}

34 changes: 34 additions & 0 deletions test/github_issue_1094.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt
//
// See: https://github.com/cppalliance/decimal/issues/1094

#include <boost/decimal.hpp>
#include <boost/core/lightweight_test.hpp>

using namespace boost::decimal;

int main()
{
BOOST_TEST(boost::decimal::fegetround() == _boost_decimal_global_rounding_mode);
BOOST_TEST(boost::decimal::fegetround() == _boost_decimal_global_runtime_rounding_mode);

#ifndef BOOST_DECIMAL_NO_CONSTEVAL_DETECTION

BOOST_TEST(fesetround(rounding_mode::fe_dec_upward) == rounding_mode::fe_dec_upward);
BOOST_TEST(boost::decimal::fegetround() != _boost_decimal_global_rounding_mode);
BOOST_TEST(boost::decimal::fegetround() == _boost_decimal_global_runtime_rounding_mode);
BOOST_TEST(boost::decimal::fegetround() == rounding_mode::fe_dec_upward);

#else

BOOST_TEST(fesetround(rounding_mode::fe_dec_upward) == _boost_decimal_global_rounding_mode);
BOOST_TEST(boost::decimal::fegetround() == _boost_decimal_global_rounding_mode);
BOOST_TEST(boost::decimal::fegetround() == _boost_decimal_global_runtime_rounding_mode);
BOOST_TEST(boost::decimal::fegetround() == rounding_mode::fe_dec_default);

#endif

return boost::report_errors();
}
Loading