<cfenv> Support :: Boost.Decimal

# `<cfenv>` Support

| 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.

1. Downward

2. To nearest (with ties to even)

3. To nearest from zero

4. Toward zero

5. Upward

| 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 `BOOST_DECIMAL_NO_CONSTEVAL_DETECTION` is not defined.

    #include <boost/decimal/cfenv.hpp>

    namespace boost {
    namespace decimal {

    enum class rounding_mode : unsigned
    {
        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
    };

    rounding_mode fegetround() noexcept;

    // Returns the rounding mode that has been set
    //
    // If your compiler defines BOOST_DECIMAL_NO_CONSTEVAL_DETECTION,
    // this function will return the default rounding mode
    // to alert you that the rounding mode has NOT changed
    rounding_mode fesetround(rounding_mode round) noexcept;

    } //namespace decimal
    } //namespace boost

| Much like `std::fesetround`, `boost::decimal::fesetround` is not thread safe.

Example 1. This example demonstrates how to set, get, and effects of the global rounding mode.

    // Copyright 2024 - 2025 Matt Borland
    // Distributed under the Boost Software License, Version 1.0.
    // https://www.boost.org/LICENSE_1_0.txt
    //
    // This example demonstrates how to set and get the global rounding mode
    // as well as the effects on numerical results

    #include <boost/decimal/decimal32_t.hpp>    // For type decimal32_t
    #include <boost/decimal/literals.hpp>       // For decimal literals
    #include <boost/decimal/cfenv.hpp>          // For access to the rounding mode functions
    #include <boost/decimal/iostream.hpp>       // Decimal support to <iostream>
    #include <iostream>

    void print_rounding_mode(const boost::decimal::rounding_mode current_mode)
    {
        // All 5 rounding modes are defined by the enum rounding_mode
        using boost::decimal::rounding_mode;

        switch (current_mode)
        {
            case rounding_mode::fe_dec_downward:
                std::cout << "fe_dec_downward\n";
                break;
            case rounding_mode::fe_dec_to_nearest:
                std::cout << "fe_dec_to_nearest\n";
                break;
            case rounding_mode::fe_dec_to_nearest_from_zero:
                std::cout << "fe_dec_to_nearest_from_zero\n";
                break;
            case rounding_mode::fe_dec_toward_zero:
                std::cout << "fe_dec_toward_zero\n";
                break;
            case rounding_mode::fe_dec_upward:
                std::cout << "fe_dec_upward\n";
                break;
        }
    }

    int main()
    {
        // The rounding mode can only be changed at run-time if the compiler supports
        // 1. C++20 std::is_constant_evaluated()
        // 2. Intrinsics that do the same
        // If neither of the above are defined the library defines BOOST_DECIMAL_NO_CONSTEVAL_DETECTION

        // The current rounding mode can be queried with boost::decimal::fegetround
        const boost::decimal::rounding_mode default_rounding_mode = boost::decimal::fegetround();
        std::cout << "The default rounding mode is: ";
        print_rounding_mode(default_rounding_mode);

        // To set a new rounding mode use boost::decimal::fesetround
        // fesetround returns current mode after updating the global state
        //
        // If your compiler set defines BOOST_DECIMAL_NO_CONSTEVAL_DETECTION the global state can not be updated,
        // so this can be a useful check to make sure that state is what you expect it to be
        auto new_rounding_mode = boost::decimal::fesetround(boost::decimal::rounding_mode::fe_dec_upward);
        std::cout << "The current rounding mode is: ";
        print_rounding_mode(new_rounding_mode);

        #ifndef BOOST_DECIMAL_NO_CONSTEVAL_DETECTION

        using namespace boost::decimal::literals;
        using boost::decimal::decimal32_t;

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

        std::cout << "lhs equals: " << lhs << '\n'
                  << "rhs equals: " << rhs << '\n';

        // 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 decimal32_t upward_res {lhs + rhs};
        std::cout << "  Sum with upward rounding: " << upward_res << '\n';

        new_rounding_mode = boost::decimal::fesetround(boost::decimal::rounding_mode::fe_dec_downward);
        std::cout << "The current rounding mode is: ";
        print_rounding_mode(new_rounding_mode);

        // Similar to above in the downward rounding mode any subtraction will result in at least a one ULP difference
        const decimal32_t downward_res {lhs - rhs};
        std::cout << "Sum with downward rounding: " << downward_res << '\n';

        #endif // BOOST_DECIMAL_NO_CONSTEVAL_DETECTION
    }

Expected Output:

    The default rounding mode is: fe_dec_to_nearest
    The current rounding mode is: fe_dec_upward
    lhs equals: 5e+50
    rhs equals: 4e+40
      Sum with upward rounding: 5.000001e+50
    The current rounding mode is: fe_dec_downward
    Sum with downward rounding: 4.999999e+50

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

Example 2. This example demonstrates how the compile time rounding mode can be set, and it’s effects on compile time and run time evaluation.

    // Copyright 2024 Matt Borland
    // Distributed under the Boost Software License, Version 1.0.
    // https://www.boost.org/LICENSE_1_0.txt
    //
    // To define a global compile-time rounding mode
    // you must define the macro before inclusion of *ANY* decimal header
    #define BOOST_DECIMAL_FE_DEC_DOWNWARD

    #include <boost/decimal/decimal32_t.hpp>    // For type decimal32_t
    #include <boost/decimal/literals.hpp>       // For decimal literals
    #include <boost/decimal/iostream.hpp>       // For decimal <iostream> support
    #include <boost/decimal/cfenv.hpp>          // For rounding mode access
    #include <iostream>

    void print_rounding_mode(const boost::decimal::rounding_mode current_mode)
    {
        // All 5 rounding modes are defined by the enum rounding_mode
        using boost::decimal::rounding_mode;

        switch (current_mode)
        {
            case rounding_mode::fe_dec_downward:
                std::cout << "fe_dec_downward\n";
                break;
            case rounding_mode::fe_dec_to_nearest:
                std::cout << "fe_dec_to_nearest\n";
                break;
            case rounding_mode::fe_dec_to_nearest_from_zero:
                std::cout << "fe_dec_to_nearest_from_zero\n";
                break;
            case rounding_mode::fe_dec_toward_zero:
                std::cout << "fe_dec_toward_zero\n";
                break;
            case rounding_mode::fe_dec_upward:
                std::cout << "fe_dec_upward\n";
                break;
        }
    }

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

        // This uses one of the same examples from our runtime rounding mode example
        // Now we can see the effects on the generation of constants,
        // since we can static_assert the result

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

        std::cout << "The default rounding mode is: ";
        print_rounding_mode(boost::decimal::rounding_mode::fe_dec_default);

        // Here we can see that the rounding mode has been set to something besides default
        // without having had to call fesetround
        //
        // This works with all compilers unlike changing the rounding mode at run-time
        std::cout << "The current rounding mode is: ";
        print_rounding_mode(boost::decimal::fegetround());
    }

Expected Output

    The default rounding mode is: fe_dec_to_nearest
    The current rounding mode is: fe_dec_downward

| 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.