1 Introduction
Floating-point exceptions are unrelated to C++ exceptions; C++ programs cannot catch and handle floating-point exceptions such as division by zero or overflow. For example, consider the following code:
double i_am_zero = 0.0;
try { double kia = 120.0 / i_am_zero; std::cout << "kia = " << kia << std::endl;}catch (...) { std::cout << "An exception occurred!\n";}
Here, kia will be an invalid floating-point number, but no exception can be caught. In most implementations, the execution of the C++ program will not be interrupted; however, kia is actually INF, no longer a valid value, and subsequent calculations will be meaningless. Therefore, when dealing with floating-point calculations, programmers must be cautious and repeatedly check the floating-point parameters involved in the calculations, such as checking if the divisor is zero before performing division.
C++ cannot control the low-level details of floating-point operations, which are typically managed by hardware (the CPU’s floating-point unit) or runtime libraries of the compiler (some CPUs require external floating-point libraries to assist with floating-point operations). However, C++ programmers are not entirely without options; some compilers provide methods to convert floating-point exceptions into C++ exceptions. The GNU libc library offers a function feenableexcept() to enable floating-point exceptions, and using GCC with the -fnon-call-exceptions compilation option can convert floating-point exceptions into user-defined C++ exceptions. MSVC also provides a similar mechanism, enabling floating-point exceptions through the _control87() function and catching the resulting structured exceptions from Windows using the compiler extension __try... __except syntax:
unsigned int old_control = _controlfp(0, 0); // Get current control word
// Enable exception traps
_controlfp(0, _EM_ZERODIVIDE | _EM_OVERFLOW | _EM_INVALID);
__try { double kia = 120.0 / i_am_zero; std::cout << "kia = " << kia << std::endl;}__except (GetExceptionCode() == STATUS_FLOAT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { std::cout << "An exception occurred!" << std::endl;}
// Restore original settings
_controlfp(old_control, 0xFFFFFFFF);
On Linux systems, Clang and GCC provide similar methods, allowing C++ programs to use SIGFPE to capture exceptions raised by the operating system:
fpu_control_t cw;
_FPU_GETCW(cw); // Get current control word
// Enable division by zero, invalid operation, overflow exceptions
cw &= ~(_FPU_MASK_ZM | _FPU_MASK_IM | _FPU_MASK_OM);
_FPU_SETCW(cw);
double i_am_zero = 0.0;
double kia = 120.0 / i_am_zero; // On Linux, this usually results in the program receiving a SIGFPE signal
std::cout << "You should not see this line printed" << std::endl;
As the saying goes, there is always a way out. However, the problem is significant. Different operating systems and compilers provide different extension mechanisms, making the code completely non-portable. It requires preprocessor directives to control the platform compatibility of the code, which is inconvenient and difficult to debug.
2 C++ Floating-Point Environment
2.1 Introduction
C++11 introduced the floating-point environment (<cfenv>) to address this issue. When floating-point operations trigger floating-point exceptions, the state of the floating-point environment changes. The C++ floating-point environment allows programmers to query and control the low-level details of floating-point operations, mainly including:
-
1. Floating-point exception flags: Records various exceptions that occur during floating-point operations, such as division by zero, overflow, invalid operations, etc.
-
2. Rounding direction: Controls the rounding mode of floating-point operation results, whether rounding towards positive infinity, negative infinity, zero, or the nearest representable value.
The C++ floating-point environment abstracts the “floating-point exception flag status” and “rounding direction control” of the floating-point unit into an “environment” that can be accessed and modified by the program. The <cfenv> header file mainly contains three parts: first, two type definitions representing the overall floating-point environment and the state of all floating-point flags:
-
fenv_t: Represents the type of the entire floating-point environment
-
fexcept_t: Represents the type of all floating-point status flags (division by zero, overflow, invalid operations, etc.)
Secondly, there is a set of functions to manage the floating-point environment, and finally, several macro constants are defined, which provide basic functionality for managing floating-point exceptions, rounding direction, and the entire floating-point environment.
2.2 Floating-Point Exceptions
The floating-point exceptions referred to in the floating-point environment are actually a set of status flags. When a certain exception occurs, the corresponding flag will be set. The exception flags will remain until explicitly cleared. The C++ floating-point environment defines six constant macros to represent these exception types:
-
FE_DIVBYZERO: Division by zero exception.
-
FE_INEXACT: Inexact result exception. This is the most common case, occurring when the result cannot be represented exactly and must be rounded.
-
FE_INVALID: Invalid operation exception. For example, taking the square root of a negative number (`sqrt(-1.0)`), `0.0 / 0.0` (resulting in NaN).
-
FE_OVERFLOW: Overflow exception. The result’s magnitude is too large, exceeding the representable range.
-
FE_UNDERFLOW: Underflow exception. The result is non-zero but too small in magnitude to be represented with normal precision.
-
FE_ALL_EXCEPT: A collection of all the above exceptions. Typically used to clear or check all exceptions at once.
In addition to constants, there is a set of functions to manipulate these flags, such as setting and clearing a specific flag. These functions are:
-
int feclearexcept(int excepts);: Clears the specified exception flags.
-
int fetestexcept(int excepts);: Tests whether the specified exception flags are set. The return value is the bitwise OR of the exceptions that have been set in excepts.
-
int feraiseexcept(int excepts);: **Simulates** and sets the specified exception. This does not actually perform an operation that would cause that exception, but sets the flag, commonly used for testing.
-
int fegetexceptflag(fexcept_t *flagp, int excepts);: Saves the state of the specified exception flags to the object pointed to by flagp.
-
int fesetexceptflag(const fexcept_t *flagp, int excepts);: Restores the state of the exception flags from the object pointed to by flagp.
2.3 Rounding Direction
Rounding direction controls the rounding mode of floating-point operations. The C++ floating-point environment defines four rounding directions through constant macros:
-
FE_DOWNWARD: Rounds towards negative infinity.
-
FE_TONEAREST: Rounds towards the nearest representable value. This is the default mode and the most commonly used mode compliant with the IEEE 754 standard.
-
FE_TOWARDZERO: Rounds towards zero (i.e., truncation).
-
FE_UPWARD: Rounds towards positive infinity.
Two functions are also provided to get the current rounding mode and set the rounding mode:
-
int fegetround(void);: Gets the current rounding direction.
-
int fesetround(int round);: Sets the rounding direction. Returns 0 on success, non-zero on failure.
2.4 Floating-Point Environment Management
Floating-point environment management provides functions for enabling and restoring the floating-point environment, as well as saving the floating-point environment. Through these functions, the entire floating-point environment (including all exception flags and rounding direction) can be saved and restored at once.
-
int fegetenv(fenv_t *envp);: Saves the current floating-point environment to `envp`.
-
int fesetenv(const fenv_t *envp);: Restores the floating-point environment from `envp`.
-
int feholdexcept(fenv_t *envp);: Saves the current environment to `envp`. The side effect of this function is to clear all exception flags and stop the current exception settings (ignoring all exceptions, not generating signals/SIGFPE).
-
int feupdateenv(const fenv_t *envp);: Sets the raised exceptions according to the current exception flags, then restores the floating-point environment from the previously saved `envp`.
The feholdexcept() function is very useful for temporarily disabling exception traps, and when used in conjunction with the feupdateenv() function, it can restore the previously stopped exception settings at a later time.
3 Typical Use Cases of Floating-Point Environment
3.1 Diagnosis and Debugging
Suppose you have written a piece of numerical computation code, but the final result is INF or NAN, and the code is very complex, making it difficult to identify the problem through code review. At this point, you can use the auxiliary functions provided by the floating-point environment to locate where the calculation went wrong. The specific approach is to insert code to check the exception flags at the appropriate locations based on the specific results, gradually pinpointing the exact location of the problem, as shown in the following code example:
// Clear all exception flags
std::feclearexcept(FE_ALL_EXCEPT);
double a = 1.0;
double b = 0.0;
double c = a / b; // This will raise FE_DIVBYZERO
double d = -1.0;
double e = std::sqrt(d); // This will raise FE_INVALID
// Check which exceptions occurred
if (std::fetestexcept(FE_DIVBYZERO)) {std::cout << "Division by zero exception occurred!" << std::endl;}
if (std::fetestexcept(FE_INVALID)) {std::cout << "Invalid operation exception occurred!" << std::endl;}
// Clear exceptions to prepare for subsequent calculations
std::feclearexcept(FE_ALL_EXCEPT);
It is important to note that if you are diagnosing the Release version of the code, the compiler may perform aggressive optimizations on the calculation process, causing the code to not align with the actual execution locations, affecting the choice of where to insert the check code. The C++ standard recommends controlling whether access to and modification of the floating-point environment is allowed through compilation macros, but it ultimately depends on the compiler’s implementation. You need to enable this switch using #pragma:
#pragma STDC FENV_ACCESS ON // GCC/Clang
//#pragma fenv_access (on) // MSVC
3.2 Controlling Rounding Direction
Generally, programmers do not need to pay direct attention to the rounding direction of floating-point numbers (this is usually considered by the authors of the numerical computation libraries used). However, it is important to know that the rounding direction of floating-point numbers can significantly affect the calculation results, especially in scenarios requiring high precision or deterministic results, where the rounding direction can greatly impact the results of cumulative calculations. The following example demonstrates controlling the rounding direction and its impact on results:
std::fenv_t env;
std::fegetenv(&env); // Save current environment
std::fesetround(FE_TOWARDZERO); // Set to round towards zero (truncation)
double x = 1.5;
double y = 2.5;
std::cout << "Rounding towards zero: " << std::nearbyint(x) << ", " << std::nearbyint(y) << std::endl; // Outputs 1, 2
std::fesetenv(&env); // Restore original environment
// Now it should be the default FE_TONEAREST
std::cout << "Rounding to nearest: " << std::nearbyint(x) << ", " << std::nearbyint(y) << std::endl; // Outputs 2, 2
The std::nearbyint() function rounds a floating-point number to the nearest integer value according to the current floating-point rounding mode and returns the result in floating-point format. From the output, we can see the impact of the rounding mode on the results.
The IEEE 754 standard defines four main rounding directions, which are rarely used in general program development. They have wide applications in finance, numerical computation, and scientific analysis. Some algorithm implementations even rely on specific rounding modes to behave correctly. Generally, or by default, the rounding mode for floating-point numbers is FE_TONEAREST, which is the most precise general rounding mode.
3.3 Safe Exception Handling
If the entire floating-point environment is set to exception trap mode, and you wish to temporarily disable floating-point exception traps in a section of code (to prevent the program from crashing), only checking and handling exceptions after the calculations are complete, you can save the environment and enter a “non-stop” mode to execute your code, restoring the original floating-point environment settings when your code exits. For example:
void risky_calculation() { double a = 1.0 / 0.0; // May raise FE_DIVBYZERO double b = std::log(0.0); // May raise FE_DIVBYZERO (result is -inf)}
int main() { std::fenv_t env;
// Save environment and enter "non-stop" mode std::feholdexcept(&env);
risky_calculation(); // Execute potentially problematic code
// Check if any exception flags were set during execution int raised_excepts = std::fetestexcept(FE_ALL_EXCEPT);
if (raised_excepts) { std::cout << "Exceptions detected in risky_calculation: " << raised_excepts << std::endl; std::feclearexcept(FE_ALL_EXCEPT); // Clear the exceptions we just detected }
std::feupdateenv(&env); // Restore original environment
return 0;}
4 Conclusion
Before the introduction of the floating-point environment, developers had to make difficult choices between portability, performance, and code complexity. The C++11 floating-point environment greatly simplifies this process, providing a unified and powerful mechanism for handling floating-point exceptions. The C++11 floating-point environment library offers programmers a standardized, portable tool for monitoring and controlling the micro-behavior of floating-point operations. Through it, you can precisely know what happens during calculations (exception flags) and control how calculations are performed (rounding modes). The features of this library include:
-
Standardized interface with good portability
-
Fine-grained control of exception flags
-
Good integration with the standard library
-
Support for saving and restoring environment states
However, currently, the C++ floating-point environment mainly handles flags in non-trap mode; by default, they only set and check flags without interrupting program execution. To generate interrupts, you still need support from system APIs, such as structured exceptions in Windows or SIGFPE on Linux.
Additionally, it is important to note that frequently reading and writing the floating-point environment can incur performance overhead, and should be avoided in performance-critical loops.
References
[1] https://cppreference.cn/w/cpp/numeric/fenv
[2] ISO/IEC 14882:2011
[3] LWG 3905 https://cplusplus.github.io/LWG/issue3905
Information, Code, and Previous ArticlesC++’s maybe_unused specifierC++’s monadic interfaceC++ 20’s designated initializersC++’s covariant return typesC++’s size and ssize functionsC++’s latch and barrierC++’s semaphoreC++’s lock() and try_lock() functionsC++’s various locksC++’s various std::mutexC++’s spanC++’s copy elisionC++’s time library part eight: format and formattingC++ 20’s PIC++’s [[noreturn]] specifierHow C++ declares lambda as friendsC++’s cache line interfaceC++’s clamp functionC++’s three and five rulesC++’s strict aliasing rulesC++’s construct_at functionContent and Notification Summary