Inspirel banner

Global Objects and Exceptions

Introduction

In C++ global objects are created before the first instruction in main(). This fact is often used to perform many kinds of preliminary activities like opening log files, connecting to databases, preparing memory pools and so on. The ability of executing some code as initialization before the program moves to its "main" business is also available in other languages like Ada and Pascal.

Problem

The problem with global objects in C++ is that there is no obvious way to catch exceptions that originate from their constructors. In other words, there is no try/catch construct that could be directly applied to that "introductory" part of the program. As a result, if any of the global objects happens to throw an exception from its constructor, the programmer does not even have any chance to react, as the following example shows:

#include <iostream>

class MyException {};

class SomeClass
{
public:
    SomeClass()
    {
        // oops, something bad happens in the constructor
        throw MyException();
    }

    // ...
};

SomeClass globalObject;

int main()
{
    try
    {
        // main business, globalObject is already constructed
        // ...
    }
    catch (...)
    {
        std::cerr << "oops\n";
    }
}

The program is correct as far as the language is concerned, but there is no hope do run it in any reasonable way:

$ g++ my_program.cpp -o my_program
$ ./my_program
terminate called after throwing an instance of 'MyException'
Abort trap
$ 

Solution

In order to be able to react somehow to the problems reported by the global object's constructor, the programmer has to apply some form of try/catch to the place where the problem originates from.

One naive possibility is to intrusively modify the constructor itself and handle the problem right there. This approach has the important disadvantage - the programmer should be actually able to modify the source code of the given class, which might not be possible with third-party components or components that are widely used.

Another approach, and the one which is recommended, relies on the ability to wrap the global object (or even many such objects) in a bigger containing object and use the function-try-block form to guard the initialization of the member subobjects.

The following example presents this approach:

#include <iostream>
#include <cstdlib>

class MyException {};

class SomeClass
{
public:
    SomeClass()
    {
        // oops, something bad happens
        throw MyException();
    }

    // ...
};

struct GlobalWrapperType
{
    GlobalWrapperType()
        try : memberObject()
    {
    }
    catch (...)
    {
        std::cerr << "oops, problem when constructing global objects\n";

        // handle the error accordingly

        std::exit(EXIT_FAILURE);
    }

    SomeClass memberObject;
};

GlobalWrapperType globalWrapper;

int main()
{
    try
    {
        // main business, globalObject is already constructed
        // ...
    }
    catch (...)
    {
        std::cerr << "oops\n";
    }
}

Above, GlobalWrapperType structure is an artificially created wrapper for the global object. The sole purpose of this wrapper is to provide a place where the exception handling can be hooked - the appropriate try/catch construct is called a function-try-block because it contains the whole constructor of the wrapper, including its initializer list, where the actual object is created.

This way exceptions from many global objects can be handled - it is enough to put all vulnerable global objects in a single wrapper and initialize them together in the same initializer list. As a bonus, those member objects that where properly constructed at the time of exception will be automatically destroyed.

An important point here is that the exception would be automatically re-thrown if the control reached the end of the handler. This makes sense, because as far as the GlobalWrapperType is concerned, one of its subcomponents failed and therefore the whole object cannot be properly constructed. Re-throwing an exception is a way for the wrapper to express the "I have failed too" message to the enclosing environment, but in this case that would just end up with abort, as in the initial example. For this reason, the program is just finished. Putting all relevant global objects together in a single wrapper can make this part easier to handle.

The only remaining problem is that the object that was supposed to be global is now part of some other object, and therefore its effective name has changed from globalObject to globalWrapper.memberObject. For maximum transparency the following alias can be added just before the main() function in the last example:

SomeClass & globalObject = globalWrapper.memberObject;

This way, the global object is still visible with the same name and the rest of the program can keep using it without any changes. The only effective change is that now there is some place where the program can react to errors coming from construction of its global objects.

Did you find this article interesting? Share it!

Bookmark and Share