Floating-point exceptions
During a numerical calculation various kinds of run-time errors may occur. In C++, such an error may be indicated via floating-point exceptions or via the (global but thread-local) variable errno. Floating-point exceptions are completely unrelated to C++ exceptions. When an floating-point exception is raised by an erroneous operation, the exception is simply noted in the floating-point status word, and the program continues. The operation produces a default value, which depends on the exception (see the table below). C++11 introduces with the new header file cfenv
functions to check and to manipulate the floating-point environment and the floating-point status flags. In this way, a program can find out which exceptions have been raised. The value of the macro math_errhandling
indicates the type of error handling that is performed by the floating-point operators and functions.
The IEEEĀ 754 standard defines five floating-point exceptions:
- Invalid operation, e.g., square root of a negative number, which returns quiet NaN by default.
- Division by zero, i.e., an operation on finite operands gives an exact infinite result, e.g., $1/0$ or $\log(0)$), which returns $\pm\infty$ by default.
- Overflow, i.e., a result that is too large to be represented correctly by a floating-point number, which returns $\pm\infty$ by default (for round-to-nearest mode).
- Underflow, i.e., a result that is very small (outside the normal range) and is inexact, which returns a denormalized value by default.
- Inexact, which returns correctly rounded result by default.
The following program raises various floating-point exceptions and illustrates how to detect them. First, one has to tell the compiler that the floating-point environment will be investigated via the #pragma STDC FENV_ACCESS ON
, which forces the compiler to prevent some optimizations, which may invalidate the floating-point status. The function feclearexcept
clears the specified floating-point status flags, while fetestexcept
determines which of the specified floating-point status flags are set. Floating-point exceptions are represented by macro constants FE_INVALID
etc. The program’s expected output is:
MATH_ERRNO is set MATH_ERREXCEPT is set 1.0/0.0 = inf ERROR: division by zero 2^2000 = inf ERROR: numerical overflow 1/2^2000 = 0 ERROR: numerical underflow sqrt(-1) = -nan ERROR: invalid result
#pragma STDC FENV_ACCESS ON
#include <cstdlib>
#include <iostream>
#include <cfenv>
#include <cmath>
void test_fp_exceptions() {
bool error=false;
if (std::fetestexcept(FE_DIVBYZERO)) {
error=true;
std::cerr << "ERROR: division by zero\n";
}
if (std::fetestexcept(FE_OVERFLOW)) {
error=true;
std::cerr << "ERROR: numerical overflow\n";
}
if (std::fetestexcept(FE_UNDERFLOW)) {
error=true;
std::cerr << "ERROR: numerical underflow\n";
}
if (std::fetestexcept(FE_INVALID)) {
error=true;
std::cerr << "ERROR: invalid result\n";
}
if (not error)
std::cerr << "no error\n";
std::feclearexcept(FE_ALL_EXCEPT);
std::cerr << '\n';
}
int main() {
double zero=0.0;
std::cout << "MATH_ERRNO is "
<< (math_errhandling & MATH_ERRNO ? "set" : "not set") << '\n'
<< "MATH_ERREXCEPT is "
<< (math_errhandling & MATH_ERREXCEPT ? "set" : "not set") << '\n'
<< '\n';
std::feclearexcept(FE_ALL_EXCEPT);
std::cout << "1.0/0.0 = " << 1.0/zero << '\n';
test_fp_exceptions();
std::cout << "2^2000 = " << std::pow(2., 2000) << '\n';
test_fp_exceptions();
std::cout << "1/2^2000 = " << std::pow(2., -2000) << '\n';
test_fp_exceptions();
std::cout << "sqrt(-1) = " << std::sqrt(-1.) << '\n';
test_fp_exceptions();
return EXIT_SUCCESS;
}
TestingĀ explicitly for floating-point exceptions can become tedious. The GNU C library provides the functions feenableexcept, fedisableexcept, and fegetexcept, to enable (and disable) traps for floating-point exceptions. This means, a floating-point exception is turned into a signal, which may be caught by a signal handler, as shown by the following program, which requires the GNU C++ compiler to compile.
#pragma STDC FENV_ACCESS ON
#include<cstdlib>
#include<iostream>
#include<cfenv>
#include<fenv.h>
#include<signal.h>
void floating_point_handler(int signal, siginfo_t *sip, void *uap) {
std::cerr << "floating point error at " << sip->si_addr << " : ";
int code=sip->si_code;
if (code==FPE_FLTDIV)
std::cerr << "division by zero\n";
if (code==FPE_FLTUND)
std::cerr << "underflow\n";
if (code==FPE_FLTOVF)
std::cerr << "overflow\n";
if (code==FPE_FLTINV)
std::cerr << "invalid result\n";
std::abort();
}
int main() {
std::feclearexcept(FE_ALL_EXCEPT);
feenableexcept(FE_DIVBYZERO | FE_UNDERFLOW | FE_OVERFLOW | FE_INVALID);
struct sigaction act;
act.sa_sigaction=floating_point_handler;
act.sa_flags=SA_SIGINFO;
sigaction(SIGFPE, &act, NULL);
double zero=0.0;
double one=1.0;
std::cout << "1.0/1.0 = " << one/one << '\n';
std::cout << "1.0/0.0 = " << one/zero << '\n';
std::cout << "1.0/1.0 = " << one/one << '\n';
return EXIT_SUCCESS;
}
Usually a signal handler will not do much more than emitting an error message, performing some clean-up work, and aborting the program, in particular, when the floating-point error is fatal, e.g., division by zero. Furthermore, when the program exits the signal handler, the floating point-exception is raised again. Consequently, the signal handler is called again, which results into an infinite loop of calls to the signal handler. A solution to this problem is to employ non-local jumps via sigsetjmp and siglongjmp to implement an exception mechanism, as illustrated below.
#pragma STDC FENV_ACCESS ON
#include<cstdlib>
#include<iostream>
#include<cfenv>
#include<fenv.h>
#include<signal.h>
#include<setjmp.h>
static sigjmp_buf jmpbuf;
void floating_point_handler(int signal, siginfo_t *sip, void *uap) {
std::cerr << "floating point error at " << sip->si_addr << " : ";
int code=sip->si_code;
if (code==FPE_FLTDIV)
std::cerr << "division by zero\n";
if (code==FPE_FLTUND)
std::cerr << "underflow\n";
if (code==FPE_FLTOVF)
std::cerr << "overflow\n";
if (code==FPE_FLTINV)
std::cerr << "invalid result\n";
feenableexcept(FE_DIVBYZERO | FE_UNDERFLOW | FE_OVERFLOW | FE_INVALID);
siglongjmp(jmpbuf, 1);
}
int main() {
std::feclearexcept(FE_ALL_EXCEPT);
feenableexcept(FE_DIVBYZERO | FE_UNDERFLOW | FE_OVERFLOW | FE_INVALID);
struct sigaction act;
act.sa_sigaction=floating_point_handler;
act.sa_flags=SA_SIGINFO;
sigaction(SIGFPE, &act, NULL);
double zero=0.0;
double one=1.0;
if (sigsetjmp(jmpbuf, 1)==0) { // try
std::cout << "1.0/1.0 = " << one/one << '\n';
std::cout << "1.0/0.0 = " << one/zero << '\n';
std::cout << "1.0/1.0 = " << one/one << '\n';
} else { // catch
std::cerr << "some error occurred\n";
}
return EXIT_SUCCESS;
}
Employing the techniques for catching floating-point exceptions as shown above, erroneous numerical calculations become easy to detect and make it possible to deal with them accordingly.