Error handling & exceptions

Contents

  • "manual error handling"
  • exception based
  • std::error_code
  • std::optional
  • logging (& context)

"Manual Error Handling"

Methods

  • global error code - errno, ::glError
  • special return values, HRESULT

Global Error Code

  • Windows - ::GetLastError()
  • POSIX - errno, perror()
  • OpenGL - glGetError()

Only an error code - must always be looked up

auto handle = ::CreateFileMapping(file, NULL, PAGE_READWRITE,
        0, 0, 0);
if (!handle) {
    auto error = ::GetLastError();
    std::cout << "Creating file mapping: " << error << std::endl;
    return;
}
auto memory = ::MapViewOfFile(handle, FILE_MAP_READ, 0, 0, 0);
if (!memory) {
    auto error = ::GetLastError();
    std::cout << "Error mapping view: " << error << std::endl;
}

Even worse if we don't have a nice special value for failure, like nullptr for pointers.

HANDLE handle;
auto has_error = ::CreateFileMapping(file, NULL, PAGE_READWRITE,
    0, 0, 0, &handle);
if (has_error) {
    auto error = ::GetLastError();
    std::cout << "Creating file mapping: " << error << std::endl;
    return;
}
POINTER memory;
has_error = ::MapViewOfFile(handle, FILE_MAP_READ, 0, 0, 0,
    &memory);
if (has_error) {
    auto error = ::GetLastError();
    std::cout << "Error mapping view: " << error << std::endl;
}

And all we wanted was

auto handle = ::CreateFileMapping(file, NULL, PAGE_READWRITE,
    0, 0, 0);
/*
if (!handle) {
    auto error = ::GetLastError();
    std::cout << "Creating file mapping: " << error << std::endl;
    return;
}
*/
auto memory = ::MapViewOfFile(handle, FILE_MAP_READ, 0, 0, 0);
/*
if (!memory) {
    auto error = ::GetLastError();
    std::cout << "Error mapping view: " << error << std::endl;
}
*/

Last error

auto handle = ::CreateFileMapping(file, NULL, PAGE_READWRITE,
    0, 0, 0);
auto memory = ::MapViewOfFile(handle, FILE_MAP_READ, 0, 0, 0);

if (!memory) {
    auto error = ::GetLastError();
    std::cout << "Error mapping view: " << error << std::endl;
}

Which error is that?

OpenGL - glGetError()

auto handle = ::CreateFileMapping(file, NULL, PAGE_READWRITE,
    0, 0, 0);
auto memory = ::MapViewOfFile(handle, FILE_MAP_READ, 0, 0, 0);

if (!memory) {
    auto error = ::GetLastError();
    std::cout << "Error mapping view: " << error << std::endl;
}

Which error is that?

Multithreading

  • ::GetLastError() and errno are thread-local - they have a separate value for each thread in the process

Issues with manual error handling

  • it is very easy to skip the error check
  • needs special error values
  • requires adding a lot of code, reducing the clarity of the program

Variants

HRESULT hr = ::DoSomething();
if (FAILED(hr)) {

}

Exceptions

Exception is an anomalous event requiring special processing, outside of the normal flow.

Exception Handling

The process of responding to the occurance of exceptions.

Advantages

Exceptions allow error handling to happen in a separate flow, outside of the main flow of the program.

Advantages

Allow us to focus on the main flow of the program.

Throwing exceptions

float sqrt(float x) {
    // C++ allows throwing any type of value
    if (x < 0) throw 42;
    // ...
}

Catching exceptions

try { // try block
    float input;
    std::cin >> input;
    std::cout << sqrt(input) << std::endl;
}
catch (int x) { // catch clause
    std::cout << "Please enter a non-negative number" <<
        std::endl;
}
catch (std::exception& e) {
}

Exception handling in C++

What happens when an exception is thrown in C++?

  1. An exception of type T is thrown
  2. The runtime starts looking for an appropriate catch clause
  3. If there is no such catch clausestd::terminate()is called
  4. Otherwise the stack is unwound to the catch clause
  5. The catch clause is executed

Appropriate catch clause

  • Takes an exception of the same type or type that is convertible to the exception type.
  • It is on the callstack

Appropriate catch clause

  • Starts from the current function. All catch clauses are checked in order, from top to bottom. The first one matching is executed.
  • If there is no matching clause the search goes to the function that called the current function and so on.

std::exception hierarchy

std::exception is the base class of all standard C++ exceptions

std::exception

class exception {
public:
    const char* what() const {
        // returns an explanatory string
    }
};

std::exception

class exception {
public:
    const char* what() const {
        // returns an explanatory string
    }
};

std::exception hierarchy

  • std::bad_alloc
  • std::bad_cast
  • std::bad_exception
  • std::logic_error
  • std::runtime_error

Custom exceptions should be derived from std::exception

Throw by value catch by reference

NEVER catch exceptions by value

catch (std::exception e) {
    // e just got "sliced"
    // the derived object got cut off
}

Catching derived classes

catch (std::exception& e) {
    std::cout << e.what() << std::endl;
}

std::terminate

std::terminate calls the currently installed std::terminate_handler. The default std::terminate_handler calls std::abort

Stack Unwinding

The process of destroying the stack frames until the appropriate catch clause is found.

During this process all destructors of automatic variables are executed. This allows for freeing any resources.

Stack Unwinding

Throwing an exception during stack unwinding is going to call std::terminate(). This is the reason to consider destructors that throw exceptions a bad practice.

What to do with the exception?

  • let the program/user know that something has failed
  • try again
  • fallback to another solution
  • change and rethrow the exception
  • throw another exception
  • stop the program

std::terminate is called when ...

  1. an exception is thrown and not caught
  2. an exception is thrown during stack unwinding
  3. the ctor or dtor of a static or thread-local object throws an exception
  4. a function registered withstd::exitorstd::at_quick_exitthrows an exception
  5. a noexcept specification is violated
  1. a dynamic exception specification is violated and the default handler forstd::unexceptedis executed
  2. a non-default handler for std::unexpected throws an exception that violates the previously violated dynamic exception specification and the specification doesn't include std::bad_exception
  3. std::nested_exception::rethrow_nested is called for an object that isn't holding a capture exception
  4. an exception is thrown out the initial function ofstd::thread
  5. a joinablestd::threadis destroyed or assigned to
  1. An exception of type T is thrown
  2. The runtime starts looking for an appropriate catch clause
  3. If there is no such catch clausestd::terminate()is called
  4. Otherwise the stack is unwound to the catch clause
  5. The catch clause is executed

Exception Safety

A set of guidelines that class library implementers and clients use when reasoning about exception handling safety in C++ programs.

Exception Safety Guarantees

  • No-throw guarantee - failure transparency
  • Strong exception safety - commit or rollback
  • Basic exception safety - no-leak guarantee
  • No exception safety

No-throw guarantee

Operations are guaranteed to succeed. All exceptions are handled internally and are not visible to the client.

Strong exception safety

Operations can fail, but failed operations have no side effects, as if they didn't attempt to run at all.

Basic exception safety

Operations can fail, failed operations might have side effects, but no invariants are broken.

No exception safety

Oh, well, ...

How to write exception safe code

Follow the begin transaction/commit/rollback pattern

// m_End points the last element of the vector
void vector::push_back(T& x) {
    if (size() < capacity()) {
        new (m_End + 1) T(x);
        // if it throws, m_End will not be moved
        ++m_End;
        // commit the push
    } else {
        std::vector<T> newvector;
        newvector.reserve((size() + 1) * 2);
        newvector.assign(begin(), end());
        newvector.push_back(x);
        
        using std::swap;
        // commit the push_back
        swap(*this, newvector);
    }
}

Exception specifiers

Pros

  • Separete program logic from error handling
  • They can't be ignored.

Cons

  • Implementations have some overhead even when no exceptions occur
    • Windows - there is a runtime overhead per function
    • Others - there is an overhead in executable size

C++'11 error handling

  • std::error_code
  • std::optional
  • noexcept

std::error_code

void create_directory(const std::string& name, std::error_code&
error);

std::error_code error;
create_directory("path", error);
if (error)
{
    if (error == std::errc:file_exists) {
    }
}
  • std::error_code is the platform-specific error
  • std::errc::* are platform-agnostic error conditions

How to add a custom error_code

http://blog.think-async.com/2010/04/system-error-support-in-c0x-part-1.html

std::optional

  • Was expected in C++14
  • Will become available as Technical Report or in C++17

boost::optional

Allows to express that there is no such value.

sqrt(-42); ///?
template <typename T>
class optional {
    T& get();
    
    T* get_ptr();
    
    const T& get_value_or(const T& default) const;
    
    T& operator*();
    T* operator->();
    
    explicit operator bool();
};

Logging

Having only an error (with a callstack if we are lucky) often is not enough.

Logging is essential for understanding what is going on with the program.

Logging

  • There is no standard C++ library for logging.

Choosing a logging library

  • Logging is an aspect of the program
  • It crawls all over the place

Logging libraries

Criteria

  • Severities, maybe even facilities
  • log rotation
  • compile time and run-time impact

What to log?

  • Not too much (it is not free), but enough to understand what is going on.
  • All important points in the lifetime of the application.
  • Log only in case of errors - error context

Crash reporting

At the end there may be crashes in the application

Breakpad

  • portable
  • allows automatic crash dump analysis