foonathan::blog()

Thoughts from a C++ library developer.

How to handle errors in constructors without exceptions?

While browsing the C++ subreddit I’ve encountered the following comment.

I’m not going to jump on the exception discussion currently happening in the child comments. I’m just going to focus on the part where he said it is sad that C++ constructors require exceptions for error handling. So let’s assume you don’t have exception support in your application and has a constructor that needs to report an error. What do you do?


Advertisement

Obligatory disclaimer if you have a strong opinion about using exceptions: I do not advocate against using exceptions. Obligatory disclaimer if you have a strong opinion against using exceptions: I do not advocate for using exceptions.

The problem

The most prominent way of error handling is with return values. But constructors do not have a return value so it can’t be done. That’s one of the reasons exceptions were added to C++.

But there are more than one way to return a value from a function. You can use output parameters:

foo(arg_t argumwent, std::error_code& ec)
{
    if (initialization_failed(argument))
        ec = ;
}

It accepts an additional argument, an output parameter. When initialization failed, instead of throwing an exception, we simply set the error code. The caller can then check the error code and handle the error.

However, this technique has multiple drawbacks. The most obvious one is that no one is forced to check the error code, and it can be easily forgotten. But there is a more subtle one.

If an exception is thrown in a constructor, the object was never fully constructed. This means that its destructor will never be called. Furthermore, there is no way to access an object in an error state. The exception will immediately unwind the local variable.

There is a nice guarantee: If the constructor call returns, the object is considered valid. This enables the RAII idiom. Consider a class that owns some resource. The constructor acquires that resource and the destructor destroys it. We want to implement a never-empty guarantee: Every class object shall own a valid resource. Assuming you’ve solved/worked around the move semantics problem, you can easily implement the constructor:

foo(arg_t argument)
: resource(acquire_resource(argument))
{
    if (!resource)
        throw no_resource();
}

Because of the guarantee, this will ensure that each object will have a resource. When the exception is thrown, there is no object.

All this is lost when you use an output parameter for the error code. Now the destructor will be called, meaning that it has to deal with all possible error states. But also the user must be careful not to use an object in error state. It is impossible to make a never-empty guarantee. Every object has at least two states: valid and invalid.

Workaround the problem

Exceptions and error codes are recoverable error handling mechanisms. They report the error to the caller and allow the program to continue. However, recoverable error handling mechanisms require precisely that: a way to report the error. Aside from exceptions this is simply not possible in a constructor without sacrificing your object guarantees.

So the easiest way to handle errors in a constructor is simply not to use a recoverable error handling mechanism. Use one that is not recoverable, like printing a message to stderr and calling abort().

As outlined in this post, this mechanism is more appropriate for stuff like programmer errors anyway. So instead of throwing an invalid_argument exception if the int is negative, use a debug assertion.

Or a proper type so the error can’t occur.

Further, there are errors that are not recoverable by nature - like out of memory. Then just call some handler function and abort the program. The user can customize how the message is displayed to the user but can’t do much to handle it.

If you only conditionally disable exceptions, you can also use a technique I’ve dubbed exception handler.

But these are just workarounds. Some errors are recoverable and can’t be handled. So let’s solve the problem.

The solution

If you can’t use a recoverable error handling mechanism without exceptions in a constructor, then don’t use a constructor.

Wait, hear me out.

I’m not suggesting an init() function or something like that. If you do that you lose all guarantees of RAII, probably also need a destroy() function because the destructor will be called for invalid objects, and now you can just as well write a C API.

RAII isn’t hard, makes life so much easier, and has no disadvantage. Well, except for the constructor exception thing, that is.

One of C++’s features is that every language feature can be implemented by yourself, the compiler just does it for you. So let’s look at constructors.

Basically, there are two steps: First, allocate raw memory for the object. Second, call the constructor in that memory, creating the object. If the second step throws an exception, enter stack unwinding. Else schedule the destructor call.

This is also how the approach with init() and destroy() methods work: the object constructor does nothing, so the compiler just does the allocating of memory. init() and destroy() then actually create the object there.

But we don’t want to make the two states part of the object itself. Every constructed object shall be valid, the complexity of an invalid state needs to be moved elsewhere. We need a wrapper that can introduce an invalid state for us, when the object’s not there.

Such a wrapper is called optional, for example. Instead of using a constructor, we don’t provide one, make it impossible to create objects. The only way to create an object is with a static function for example. But this is a regular function, so we can use return values. In particular, it returns an optional object:

optional<foo> make(arg_t argument, std::error_code& ec)
{
    auto resource = make_resource(argument);
    if (resource)
        return foo(resource);
    return {};
}

If everything was successful, we can return an object. But in the error case, we don’t need to return an invalid object. Instead we can return an empty optional.

This API can be used like so:

std::error_code ec;
auto result = foo::make(arg, ec);
if (result)
{
    // everything alright
    
}
else
    handle_error(ec);

Now every time we get an object, it is guaranteed to be valid. The invalid state is moved elsewhere, where handling can be better implemented. So every member function and the destructor does not need to deal with an invalid state. That is, as long as the make() function only creates an object, i.e. calls the constructor, when nothing can go wrong anymore.

Better error reporting

The return value as output parameter is a bit awkward.

And not necessarily because of the problems with output parameters, those can be solved.

A better way would be to integrate that in the return value. Instead of returning an optional, use an “either value or error” class. The proposed std::expected does that and allows to handle the error more elegantly.

What about copy constructors?

This technique works well for “regular” constructors, but what about copying? It is still an operation that can possibly fail.

There are two solutions: don’t provide copy operations, only move - which won’t fail(usually) - or use the same technique again. Provide a static copy function that does the same thing, again returning optional/expected, etc.


Advertisement

Conclusion

If you do not have exceptions, reporting errors from a constructor is impossible without sacrificing guarantees. Where possible, simply use an alternative and non-recoverable way of error reporting.

If that’s not applicable, provide a static function as the only way to create the object. It does not return an object directly, but an optional type. Carefully craft the implementation, so that the actual private constructor will only be called, when no operation can fail. Then every object will be valid, just like it were the case when using exceptions.

This post was made possible by my Patreon supporters. If you'd like to support me as well, please head over to my Patreon and do so! One dollar per month can make all the difference.