Floating Point Exceptions

What happens when we do something bad? Consider this example:

Listing 77 undefined.cpp
#include <iostream>
#include <cmath>

double trouble(double x) {
    return std::sqrt(x);
}

int main() {

    double x{-1};

    double y = trouble(x);

    for (int i = 0; i < 10; ++i) {
        y += std::pow(x, i);
    }

    std::cout << y << std::endl;

}

Here, we pass -1 to trouble() which then takes the square root of it—this results in a NaN. But if we run the code, it goes merrily about its way, using that result in the later computations.

Unix uses signals to indicate that a problem has happened during the code execution. If a program created a signal handler then that signal can be trapped and any desired action can be taken.

Note

This example was only tested on a Linux machine with GCC. Other OSes or compilers might have slightly different headers or functionality.

There are a few parts to trapping a floating point exception (FPE). First we need to enable exception trapping via:

feenableexcept(FE_INVALID|FE_DIVBYZERO|FE_OVERFLOW);

That catches 3 different types of floating point exceptions—invalid, divide-by-zero, and overflows.

Note

See cppreference floating point environment for a full list of the macros of exceptions that can be trapped.

Next we need to add a handler to deal with the exception:

signal(SIGFPE, fpe_handler);

Here, SIGFPE is the standard name for a floating point exception, and fpe_handler is the name of a function that will be called when we detect a SIGFPE.

Finally, we need to write the handler, and we’d like to get a backtrace. There are two ways that we can approach this, which we see next.

Linux backtrace method

We can use the Linux backtrace() function to access the stack of our program execution. This is really a C-function, so we need to use C-style arrays here.

Here’s the new version of our code:

Listing 78 undefined_trap.cpp
#include <iostream>
#include <cmath>
#include <csignal>
#include <cfenv>
#include <execinfo.h>

void fpe_handler(int s) {
    std::cout << "floating point exception, signal " << s << std::endl;

    const int nbuf = 64;
    void *bt_buffer[nbuf];
    int nentries = backtrace(bt_buffer, nbuf);
    char **strings = backtrace_symbols(bt_buffer, nentries);

    for (int i = 0; i < nentries; ++i) {
        std::cout << i << ": " << strings[i] << std::endl;
    }

    abort();
}

double trouble(double x) {
    return std::sqrt(x);
}


int main() {

    feenableexcept(FE_INVALID|FE_DIVBYZERO|FE_OVERFLOW);

    signal(SIGFPE, fpe_handler);

    double x{-1};

    double y = trouble(x);

    for (int i = 0; i < 10; ++i) {
        y += std::pow(x, i);
    }

    std::cout << y << std::endl;

}

When we compile the code, we want to add the -g option to store the symbols in the code—this allows us to understand where problems arise:

g++ -g -o undefined_trap undefined_trap.cpp

Now when we run this, the program aborts and we see:

floating point exception, signal 8
0: ./undefined_trap() [0x401261]
1: /lib64/libc.so.6(+0x42750) [0x7f3dc35dc750]
2: /lib64/libm.so.6(+0x1435c) [0x7f3dc37d335c]
3: ./undefined_trap() [0x4012ff]
4: ./undefined_trap() [0x401347]
5: /lib64/libc.so.6(+0x2d560) [0x7f3dc35c7560]
6: /lib64/libc.so.6(__libc_start_main+0x7c) [0x7f3dc35c760c]
7: ./undefined_trap() [0x401145]
Aborted (core dumped)

This is the call stack for our program. In the brackets are the address in the program where the execution was when the FPE occurred.

Note

The addresses you see might be different, and they will depend on the compiler and OS.

These lines are ordered such that the calling function is below the function where the execution is. So it usually is best to look at the addresses near the top.

We can turn those into line numbers using addr2line:

addr2line  -e undefined_trap 0x4012ff

gives:

/home/zingale/classes/phy504/examples/floating_point/undefined_trap.cpp:23

and that line is precisely where the sqrt() is!

We can get slightly nicer output (including the function name) by doing:

addr2line  -C -f -p -e undefined_trap 0x4012ff

which gives:

trouble(double) at /home/zingale/classes/phy504/examples/floating_point/undefined_trap.cpp:23

Note

On the MathLab machines, the stack trace seems to include an offset, like:

floating point exception, signal 8
0: ./undefined_trap(+0xc03) [0x561d8799dc03]
1: /lib/x86_64-linux-gnu/libc.so.6(+0x3ef10) [0x7f5461e3df10]
2: /lib/x86_64-linux-gnu/libm.so.6(+0x11397) [0x7f5462201397]
3: ./undefined_trap(+0xccf) [0x561d8799dccf]
4: ./undefined_trap(+0xd21) [0x561d8799dd21]
5: /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f5461e20c87]
6: ./undefined_trap(+0xaaa) [0x561d8799daaa]
Aborted (core dumped)

and we need to use that offset instead with addr2line, like:

addr2line -a -f -e ./undefined_trap +0xcd5

C++23 stacktrace library

A more modern (and simpler) way to get the stacktrace is available in C++23 via the stacktrace library.

Listing 79 undefined_stacktrace.cpp
#include <iostream>
#include <cmath>
#include <csignal>
#include <cfenv>
#include <stacktrace>

void fpe_handler(int s) {
    std::cout << "floating point exception, signal " << s << std::endl;

    std::cout << std::stacktrace::current() << std::endl;

    abort();
}

double trouble(double x) {
    return std::sqrt(x);
}


int main() {

    feenableexcept(FE_INVALID|FE_DIVBYZERO|FE_OVERFLOW);

    signal(SIGFPE, fpe_handler);

    double x{-1};

    double y = trouble(x);

    for (int i = 0; i < 10; ++i) {
        y += std::pow(x, i);
    }

    std::cout << y << std::endl;

}

To build this, we need to load an extra library. For GCC 12 or 13, we can do:

g++ -g -std=c++23 -o undefined_stactrace undefined_stacktrace.cpp -lstdc++_libbacktrace

For GCC 14, we would do:

g++ -g -std=c++23 -o undefined_stactrace undefined_stacktrace.cpp -lstdc++exp