Search code examples
c++floating-pointseh

How do I use _controlfp_s when using SEH?


I have been trying to write some error-protection clauses for identifying problems in a dll which is provided to us by an third party. There may be problems in this dll (memory exceptions, floating point errors, etc), and it is advantageous to be able to identify these errors without access to the source code.

I have something put together from various SEH error handling routines, but although it works, there are several... inconsistencies with it. I'm trying to isolate each one, and I'm going to ask a question on each one individually.

This one is to do with using _controlfp_s to set and reset the floating point error behaviour.

// For floating point protection ---------
#include <float.h>      // defines of _EM_OVERFLOW, etc.
#include <string.h>     // strncpy_s & strncat_s
#include <stdlib.h>     // malloc
#include <excpt.h>      // EXCEPTION_EXECUTE_HANDLER
#include <iostream>     // cout
#include <bitset>       // bitset
#include <conio.h>      // _kbhit
#pragma fenv_access (on)
// ---------------------------------------


const unsigned int SERIOUS_FP_EXCEPTIONS = _EM_DENORMAL | _EM_ZERODIVIDE | _EM_INVALID;
const unsigned int MINOR_FP_EXCEPTIONS = _EM_OVERFLOW | _EM_UNDERFLOW | _EM_INEXACT;

int main(int argc, char[])
{
    double numerator = 1.0;
    double denominator = 0.0;
    double result = 0.0;


    unsigned int _previous_floating_point_control;
    unsigned int _current_floating_point_control;

    _controlfp_s(&_current_floating_point_control, 0, 0);
    std::cout << "Floating point word originally:       " << std::bitset<32>(_current_floating_point_control) << std::endl;
    std::cout << "New settings to add:                  " << std::bitset<32>(MINOR_FP_EXCEPTIONS) << std::endl;
    std::cout << "With the mask:                        " << std::bitset<32>(_MCW_EM) << std::endl;

    _controlfp_s(&_previous_floating_point_control, MINOR_FP_EXCEPTIONS, _MCW_EM);      // This should appear to work, according to the documentation.
    std::cout << "Floating point word cached:           " << std::bitset<32>(_previous_floating_point_control) << std::endl;

    /* This works:
    _controlfp_s(&_previous_floating_point_control, 0, 0);      // This should appear to work, according to the documentation.
    _controlfp_s(nullptr, MINOR_FP_EXCEPTIONS, _MCW_EM);      // This should appear to work, according to the documentation.
    std::cout << "Floating point word cached:           " << std::bitset<32>(_previous_floating_point_control) << std::endl;
    */

    _controlfp_s(&_current_floating_point_control, 0, 0);
    std::cout << "Floating point word used:             " << std::bitset<32>(_current_floating_point_control) << std::endl;

    __try {
        result = numerator / denominator;
        _controlfp_s(NULL, _previous_floating_point_control, _MCW_EM);
        std::cout << "No error detected." << std::endl;
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        std::cout << "Error code is:                        " << std::bitset<32>(GetExceptionCode()) << std::endl;
        if ((GetExceptionCode() & _EM_INVALID) || (GetExceptionCode() & _EM_ZERODIVIDE) || (GetExceptionCode() & _EM_DENORMAL))
            std::cout << "ERROR! Serious floating point error!" << std::endl;
        else
            std::cout << "Something else..." << std::endl;

        _controlfp_s(NULL, _previous_floating_point_control, _MCW_EM);

        _controlfp_s(&_current_floating_point_control, 0, 0);
        std::cout << "Floating point word reset to:         " << std::bitset<32>(_current_floating_point_control) << std::endl;

    }

    std::cout << "result = " << result << std::endl;

    while (!_kbhit())   // Wait until a key is pressed to close console.
    { }
}

This produces:

Floating point word originally:       00000000000010010000000000011111
New settings to add:                  00000000000000000000000000000111
With the mask:                        00000000000010000000000000011111
Floating point word cached:           00000000000010010000000000000111
Floating point word used:             00000000000010010000000000000111
ERROR! Serious floating point error!
Floating point word reset to:         00000000000010010000000000000111
result = 0

Notice that _previous_floating_point_control was actually set to the new value, so the effort to reset it at the end doesn't do anything.

This is the correct behaviour (using _controlfp_s in two steps):

Floating point word originally:       00000000000010010000000000011111
New settings to add:                  00000000000000000000000000000111
With the mask:                        00000000000010000000000000011111
Floating point word cached:           00000000000010010000000000011111
Floating point word used:             00000000000010010000000000000111
ERROR! Serious floating point error!
Floating point word reset to:         00000000000010010000000000011111
result = 0

I presume that this is a bug, and since I have a way around it, I'm happy to ignore it.

It gets worse, though: if you notice, the above example (which works) sets the floating point word to be MINOR_FP_EXCEPTIONS, and this successfully picks up the SERIOUS_FP_EXCEPTIONS. If I try replacing this:

_controlfp_s(&_previous_floating_point_control, SERIOUS_FP_EXCEPTIONS, _MCW_EM); 

Yields:

Floating point word originally:       00000000000010010000000000011111
New settings to add:                  00000000000010000000000000011000
With the mask:                        00000000000010000000000000011111
Floating point word cached:           00000000000010010000000000011111
Floating point word used:             00000000000010010000000000011000
No error detected.
result = 1.#INF


_controlfp_s(&_previous_floating_point_control, 0, _MCW_EM); 

Yields:

Floating point word originally:       00000000000010010000000000011111
New settings to add:                  00000000000000000000000000000000
With the mask:                        00000000000010000000000000011111
Floating point word cached:           00000000000010010000000000011111
Floating point word used:             00000000000010010000000000000000
Error code is:                        11000000000000000000001010110101
ERROR! Serious floating point error!
Floating point word reset to:         00000000000010010000000000011111
result = 0

And: _controlfp_s(nullptr, _MCW_EM, _MCW_EM); Yields:

Floating point word originally:       00000000000010010000000000011111
New settings to add:                  00000000000010000000000000011111
With the mask:                        00000000000010000000000000011111
Floating point word cached:           00000000000010010000000000011111
Floating point word used:             00000000000010010000000000011111
No error detected.
result = 1.#INF

So to summarise:

  • _controlfp_s does not "get" the old floating point word as a by-product of the "set", but does if you do it in a separate operation.
  • To catch serious floating point errors, you need to set _controlfp_s to look for minor f.p.e.s and NOT major ones.
  • [The problem of failing to identify divide-by-zero is in one of the other questions]

Am I just misunderstanding this whole area?


Solution

  • I think I will hazzard an answer to this one, since I've had a very useful response to my related question: How do I interpret GetExceptionCode results when using SEH?.

    Through experimentation, it seems as though the new settings to _controlfp_s are the exceptions to ignore, rather than the exceptions to react to. I found the official documentation on this... unclear.

    On the issue of returning the old value to the f.p. word when setting a new one: I believe it is a bug - or at least undesired behaviour. The documentation explicitly says that _controlfp_s will populate the old value when the mask is 0, but doesn't mention otherwise. In almost all similar functions, I would expect you to be able to do the get and the set in one operation - rather than one function which operates either as a get or a set.