Why I Catch Exceptions in Main Function in C++

Sun
22
Jan 2023

Exception handling in C++ is a controversial topic. On one hand, it can be a good means of reporting and handling errors if done correctly. For it to be free from memory leaks, all memory allocations should be wrapped in smart pointers and other acquired resources (opened files and other handles) wrapped in similar RAII objects. On the other hand, it has been proven many times that the exception mechanism in C++ works very slow. Disabling exception handling in C++ compiler options can speed up the program significantly. No wonder that game developers dislike and disable them completely.

Let’s talk about a command-line C++ program that doesn’t need to disable exception handling in compiler options. Even if it doesn’t use exceptions explicitly, some exceptions may occur, thrown by C++ standard library or some third-party libraries. When a C++ exception is thrown and uncaught, program terminates and process exit code is some large negative number. On my system it is -1073740791 = 0xC0000409.

It would be good if the program printed some error message in such case and returned some custom, clearly defined exit code. Therefore, when developing a command-line C++ program, I like to catch and handle exceptions in the main function, like this:

#include <exception>
#include <cstdio>

enum PROGRAM_EXIT {
    PROGRAM_EXIT_SUCCESS   =  0,
    PROGRAM_EXIT_FAILED    = -1,
    PROGRAM_EXIT_EXCEPTION = -2
};

int ActualProgram(int argc, char** argv) {
    ...
}

int main(int argc, char** argv) {
    try {
        return ActualProgram(argc, argv);
    }
    catch(const std::exception& ex) {
        fprintf(stderr, "ERROR: %s\n", ex.what());
        return PROGRAM_EXIT_EXCEPTION;
    }
    catch(...) {
        fprintf(stderr, "UNKNOWN ERROR.\n");
        return PROGRAM_EXIT_EXCEPTION;
    }
}

Besides that, if you develop for Windows using Visual Studio, there is another, parallel system of throwing and catching exceptions, called Structured Exception Handling (SEH). It allows you to handle “system” errors that are not C++ exceptions and would otherwise terminate your program, even when using code shown above. This kind of error can be memory access violation (using null or incorrect pointer) or integer division by zero, among others. To catch them, you can use the following code:

#include <Windows.h>

int main(int argc, char** argv) {
    __try {
        return main2(argc, argv);
    }
    __except(EXCEPTION_EXECUTE_HANDLER) {
        fprintf(stderr, "STRUCTURED EXCEPTION: 0x%08X.\n",
            GetExceptionCode());
        return PROGRAM_EXIT_EXCEPTION;
    }
}

Few additional notes are needed here. First, SEH __try-__except section cannot exist in one function with C++ try-catch. It is fine, though, to call a function doing one way of error handling from a function doing the other one. Their order is important – C++ exceptions are also caught by SEH __except, but SEH exceptions are not caught by C++ catch. So, to do it properly, you need to make your main function doing SEH __try-__except, which calls some main2 function doing C++ try-catch, which calls ActualProgram – not the other way around.

If you wonder what are the process exit codes returned by default when exceptions are not caught, the answer can be found in the documentation of GetExceptionCode macro and Windows header files. When memory access violation occurs, this function (or the entire process, if SEH exceptions are not handled) returns -1073741819 = 0xC0000005, which matches EXCEPTION_ACCESS_VIOLATION. When a C++ exception is thrown, the code is -1073740791 = 0xC0000409, which is not one of EXCEPTION_ symbols, but I found it defined as STATUS_STACK_BUFFER_OVERRUN (strange…). Maybe it would be a good idea to extend the __except section shown above to decode known exception codes and print their string description.

Finally, you need to know that integer division by zero throws a SEH exception, but floating-point division by zero does not – at least not by default. There is EXCEPTION_FLT_DIVIDE_BY_ZERO and EXCEPTION_INT_DIVIDE_BY_ZERO error code defined, but the default behavior of incorrect floating-point calculations (e.g. division by zero, logarithm of a negative value) is to return special values like Not a Number (NaN) or infinity and proceed with further calculations. This behavior can be changed, as described in “Floating-Point Exceptions”.

Comments | #windows #visual studio #c++ Share

Comments

[Download] [Dropbox] [pub] [Mirror] [Privacy policy]
Copyright © 2004-2024