Skip to content

Latest commit

 

History

History
1027 lines (777 loc) · 46.5 KB

README.adoc

File metadata and controls

1027 lines (777 loc) · 46.5 KB

Noexcept

Abstract

Noexcept is small and efficient C++11 error-handling library which does not use exception handling and does not require exception-safe environment. Some of its features are:

  • Function return values are not burdened with transporting error objects in case of a failure.

  • Errors can be identified by error code values (including std::error_code), or by static types if type safety is desired.

  • Error-neutral contexts can forward to the caller any error condition detected in lower level functions, without touching the error object.

  • Error-handling contexts may selectively handle some errors but remain neutral to others.

Because Noexcept is not intrusive to function return types, it is able to propagate failures even across third-party code which may not be able to use specific sophisticated error-reporting types. This makes Noexcept deployable even in tricky use cases where a more intrusive error handling mechanism could not be used:

  • When errors are reported from a C-style callback, or

  • from a C++ virtual function override when deriving from a third-party base type.

  • When reported errors need to cross multiple API levels, each with their own error reporting mechanism.

  • When reported errors need to cross programming language boundaries.

Distribution

Noexcept is distributed under the Boost Software License, Version 1.0.

The source code is available in this GitHub repository.

Note
Noexcept is not part of Boost.

Feedback / support

Design rationale

noexcept functions which could potentially fail must communicate the failure directly to the caller — but they also need to be able to return a value in case of success.

This naturally leads designers of error-handling libraries to creating a special return type capable of transporting any value or any error, with two design goals:

  1. Zero abstraction penalty, at the very least in the case the function succeeds.

  2. Flexibility to transport a wide range of error objects and data pertaining to error conditions.

The problem is that these are competing goals — which would force us into a compromise, the victim probably being the flexibility of the error-transporting facilities. This is not ideal for an error handling library.

In addition, such design would lead to problems specific to C and C++:

  • It is intrusive, so it couldn’t be used in many practical cases, for example when reporting errors from a C callback.

  • C++ programmers do not even agree on what string type they should use, so interoperability between the different parts of a large program is going to be a problem.

For these reasons, Noexcept does not define a special return type. Programmers may use any type whatsoever, the only requirement is that it provides a special state — a value that can be returned in case of a failure. Besides facilitating interoperability, this virtually eliminates the chance of Noexcept impacting the performance of passing values up the call chain.

When a failure is detected, the error object is created in thread-local storage rather than passed up one level at a time. It is destroyed when control reaches an error-handling function that recognizes it and recovers from the failure.

Further rationale for using TLS for transporting error objects is that errors are semantically very different from successful results. In a typical program, there may be very many results of different types produced, transformed, processed, used in different computations and algorithms, passed up and down the call chain. In contrast, the path taken by an error object is much simpler — and it is (mostly, sometimes even exclusively) of interest to only two contexts: the function that detects and reports the error, and the function which recognizes it and is able to handle it. For that reason, it makes sense to push errors to the side, so they are less intrusive yet still available for access if needed.

Note
For a comparison between various error-reporting libraries and proposals, see Alternatives to Noexcept. There is also a benchmark program.

No error gets ignored guarantee

Noexcept will never ignore an error once it is reported. The handling of any given error may be postponed (e.g. using result<T> ), but eventually the program is required to recognize each and every error object and flag it as handled. Situations which would lead to a pending error being ignored are detected and result in a call to std::terminate.

Crash course

Handling FILE errors

link:./examples/FILE_ptr_example.cpp[role=include]
  1. If an error is detected, throw_ associates the passed std::error_code object with the calling thread and converts to a 0 FILE * for the return value (as specified by the throw_return template).

  2. try_ checks for success and moves the return value of open_file (or the error it passed to throw_) into r.

  3. catch_ returns a pointer to the error object (or 0 if it is not a std::error_code) and flags it as handled.

  4. When r is destroyed, the error object is destroyed if handled, otherwise left intact for another try_/catch_ up the call chain to handle.

Important
If unhandled errors remain at the time the current thread terminates, Noexcept calls std::terminate(). Use the default for the catch_<> function template to handle any error regardless of its type.

Handling std::ifstream errors

This program shows how to use Noexcept with std::ifstream, and demonstrates the use of BOOST_NOEXCEPT_CHECK in error-neutral functions (see Programming techniques and guidelines for details).

link:./examples/ifstream_example.cpp[role=include]
  1. open_stream returns a "good" ifstream or std::error_code (the throw_ converts to an empty ifstream, as specified by the throw_return template).

  2. The BOOST_NOEXCEPT_CHECK (on the previous line) would immediately return any pending errors to the caller using return throw_(), so here we can assert(f.good()).

  3. The read_line function returns a string or another std::error_code.

  4. Thanks to the BOOST_NOEXCEPT_CHECK in read_line, here we don’t need to check for errors: any success or failure reported by read_line or open_stream will be propagated automatically.

  5. Dispatch on known errors and print the appropriate message.

  6. The commented-out section shows how boost::diagnostic_information() could be used by a more complex program to report that main() could not recognize an error object that reached it.


Propagating errors across Lua functions

This program demonstrates that errors reported by Noexcept survive intact across API and even programming language boundaries. From C++, it calls a Lua function, which calls back a C++ function. If the callback C++ function fails, the error is propagated all the way to main() where it is handled.

link:./examples/lua_example.cpp[role=include]
  1. Initialize the Lua interpreter and define a simple Lua function (provided as a C string literal) called call_do_work, which simply returns the value returned by the Lua-global do_work function, which is a C++ function.

  2. In the C++ do_work function, in case of success we return 42 back to Lua.

  3. In case of a failure, we pass do_work_error to throw_ as usual, and call lua_error.

  4. The call_lua function instructs the Lua interpreter to execute the call_do_work Lua function (which calls do_work). If do_work was successful, call_lua returns the answer; otherwise it forwards failures by return throw_(), as usual.

  5. In case do_work fails, the reported error object (which at this point has been propagated from C++, through Lua and back into C++) is handled here.


Using optional<> as a return type

This program demonstrates that Noexcept is compatible with optional<>, which is handy when we need a return value with an explicit empty state.

link:./examples/c_api_example.cpp[role=include]
  1. A C function which uses int status codes to indicate success or failure, and a float pointer to store the result.

  2. A simple error type to report failures from the C API function.

  3. The cumbersome float pointer interface of the C function is converted to boost::optional<float>. The throw_ returns an empty optional in case of failure (as specified by the throw_return template).

  4. try_ both checks for success and moves the return value of erratic_caller into r.

  5. catch_ returns a pointer to the error object (or 0 if it can not be converted to erratic_error) and flags it as handled.

Synopsis

boost/noexcept/throw.hpp

boost/noexcept/try.hpp

Reference

Report: throw_

throw_ is a utility class designed to be used in return expressions to indicate failure:

return throw_( my_error() );

or

return throw_( std::error_code { 42, my_domain } );

The passed object becomes the current error for the calling thread. The error remains current until captured into a result object (see try_). For threads that have a current error, has_current_error() returns true.

The throw_ object itself converts to any type so it can be used in return expressions in any function: if the function return type is T, the returned value is throw_return<T>::value().

The throw_return<T> template may be specialized to specify the value to be returned to the caller when throw_ is used to report a failure. The main template is defined such that:

  • If T is not bool and std::is_integral<T>::value is true, then throw_return<T>::value() returns static_cast<T>(-1).

  • Otherwise it returns T().

The throw_ class has the following members:


throw_ constructors

template <class E>
throw_( E && e ) noexcept;

template <class E>
throw_( E && e, char const * file, int line, char const * function ) noexcept;

#define THROW_(e)\
    ::boost::noexcept::throw_(e,__FILE__,__LINE__,BOOST_CURRENT_FUNCTION)
Preconditions:

!has_current_error()

Effects:

e becomes the current error for the calling thread. If file, line and function are specified, that information is recorded for diagnostic purposes (use with the THROW_ macro).

Postconditions:

has_current_error()

throw_() noexcept;
Effects:

None: the default constructor is used to continue propagating the current error.

Note
This is different from the throw_ member function of result<T>, which is used in return expressions to propagate the error stored in the result object, rather than the current error.

throw_::operator T()

template <class T>
operator T() noexcept;
Preconditions:

has_current_error()

Returns:

throw_return<T>::value().


has_current_error()

bool has_current_error() noexcept;
Returns:

If throw_ was used to set the current error for the calling thread, and if that error has not been captured by a result object (see try_), has_current_error returns true. Otherwise it returns false.


#define BOOST_NOEXCEPT_CHECK\
    { if( ::boost::noexcept_::has_current_error() )\
        return ::boost::noexcept_::throw_(); }

#define BOOST_NOEXCEPT_CHECK_VOID\
    { if( ::boost::noexcept_::has_current_error() )\
        return; }

These macros are designed to be used right after the opening { of a function to return immediately in case previous operations have resulted in an error. Please see Programming techniques and guidelines for a motivating example.


Handle: try_/catch_

Pass a function return value directly to try_ (or use current_error() in the case of void functions) if you want to handle the errors it could report:

  • If the function was successful (has_current_error() is false), use the get member function of the returned result<T> object to obtain the returned value.

  • Otherwise instead of storing a T result, the returned result<T> object captures the current error, which leaves the calling thread without a current error. Use the catch_ member function template to handle specific errors (recognized by their type).

The following convenient syntax is supported:

if( auto r=try_(f()) ) {
    //Success, use r.get() to obtain the value returned by f()
} else {
    //Handle error, see result::catch_<>
}

The result template defines the following public members:


result<T> destructor

~result() noexcept;
  • At the time a result<T> object for which has_unhandled_error() is true is destroyed:

    • If has_current_error() is also true, result<T> calls std::terminate().

    • Otherwise the error contained in *this becomes the current error for the calling thread again.

  • Otherwise (has_unhandled_error() is false), if has_error() is true, the captured error object is destroyed.

  • Otherwise the captured T value is destroyed.


result<T>::has_error()

bool has_error() const noexcept;
Returns:

true if *this contains an error object, false if it contains a value.


result<T>::has_unhandled_error()

bool has_unhandled_error() const noexcept;
Returns:

true if has_error() and the error has not yet been handled, false otherwise.

Note
The error contained in a result is marked as handled when catch_<E> is called, but only if it can be converted to type E.

result<T>::operator bool()

explicit operator bool() const noexcept;
Returns:

Equivalent to return !has_error().


result<T>::catch_()

template <class E=std::exception>
E * catch_() noexcept;
Returns:

If has_error() is true and the error object contained in *this is of type E, returns a pointer to that object; otherwise returns 0. The returned pointer becomes invalid if the result object is moved or destroyed.

Effects:

The error object captured into *this is marked as handled but only if it is of type E; see ~result().

Note
catch_<> (using the default for E) serves similar function to catch(...) when handling exceptions, however in Noexcept any error object passed to throw_ (even objects of built-in types) internally will have a std::exception component, so catch_<> is able to return a valid std::exception pointer regardless of the type passed to throw_.

result<T>::throw_()

<unspecified_return_type> throw_() noexcept;
Preconditions:

has_error().

Effects:

The error object stored into *this becomes the current error for the calling thread again.

Returns:

An object of unspecified type which converts implicitly to any type for use in return expressions. Example usage:

if( auto r=try_(f()) ) {
    //success, use r.get()
} else {
    //possibly handle some errors, then:
    return r.throw_(); //propagate the error object up the call chain
}

result<T>::throw_exception()

void throw_exception();
Preconditions:

has_error().

Effects:

Throws the error object as a C++ exception.


result<T>::get()

//Not available in result<void>:
T const & get() const;
T & get();
Effects:

If !has_error(), get() returns a reference to the object passed to try_ and moved into *this; otherwise get() calls throw_exception().


try_()

template <class T>
result<T> try_( T && res ) noexcept;

result<void> void_try_() noexcept;
Effects:
  • If !has_current_error(), res is moved into the returned result<T> object, later accessible by a call to get().

  • Otherwise the current error for the calling thread is moved into the returned result<T> object, later accessible by a call to catch_().

Postconditions:

!has_current_error().

Note
result<T> fully encapsulates the result (value or error) of the function call and has no association with the calling thread, so result<T> objects can be temporarily stored (e.g. in some queue< result<T> >) or moved to a different thread before their contents is consumed; see Programming techniques and guidelines for an example.

Programming techniques and guidelines

Selecting optimal function return types

Noexcept is able to propagate failures without imposing a specific function return type, which is important because sometimes failures need to be propagated through third-party functions whose return types are beyond our control.

When it is possible to take Noexcept into account in the design of function return value semantics, it is best to pick types that have a natural state (in the respective function domain) that can be used to detect failures without resorting to has_current_error(). In this case Noexcept can stay completely out of the way, except when detected errors are being handled.

There are many function return types which usually fit this guideline quite naturally. Here are a few examples:

  • bool

  • T * (notably FILE *)

  • shared_ptr<T>

  • unique_ptr<T>

  • optional<T>

  • ifstream

  • HANDLE (Windows system objects)

  • int and any integral type (-1 is invalid count, length, magnitude, size, width, height, file descriptor, etc.)

Tip
shared_ptr<T> deserves special mentioning here because the possibility to use custom deleters make it extremely deployable as the return value from any factory function which allocates any type of resource.

Other types that have an empty state may require more careful consideration:

  • string

  • vector<T> (and any other container type)

Case in point, string works great with functions that return a file name or user name because these may not be empty. When an empty string is a valid return value, requiring the caller to use has_current_error() to check for error is usually the best choice, but sometimes it might make sense to use a wrapper, e.g. optional<string> or shared_ptr<vector<T> >.


Enforcing successful control flow

Consider the following declarations:

std::vector<std::string> read_file( FILE * f ) noexcept;
int count_words( std::vector<std::string> const & lines ) noexcept;

The count_words function is designed to take as input the result returned by read_file, yet in a calling function we can not use function composition and simply say:

int count_words_in_file( FILE * f ) noexcept {
    return count_words( read_file(f) );
}

That’s because we must check if read_file was successful. Specifically, we need to be able to tell the difference between read_file returning an empty vector (using throw_) because it failed to read the file, and it returning an empty vector because the file was empty.

In terms of Noexcept, we could use:

int count_words_in_file( FILE * f ) noexcept {
    if( auto r = try_( read_file(f) ) )
        return count_words( r.get() );
    else
        return r.throw_();
}

This is not too bad: at least errors are pushed out of the way, and we don’t have to worry about how to propagate all possible errors up the call chain. Still, requiring users to check for errors every time they call read_file is prone to errors — and in this case it is particularly annoying, since count_words and read_file fit so well together.

Using Noexcept we can do better: we can have functions like count_words check for errors themselves. That’s because error-neutral contexts can propagate errors up the call stack using return throw_():

if( has_current_error() )
    return throw_();

For convenience, this handy if statement — surrounded by { } — is provided as the BOOST_NOEXCEPT_CHECK macro, which allows us to define count_words as:

int count_words( std::vector<std::string> const & lines ) noexcept {
    BOOST_NOEXCEPT_CHECK
    //No errors, continue with counting words.
    ....
    return n; //the number of words in lines
}

In turn, this allows the calling count_words_in_file function to safely use simple function composition:

int count_words_in_file( FILE * f ) noexcept {
    return count_words( read_file(f) );
}
Note
The macro BOOST_NOEXCEPT_CHECK_VOID can be used similarly in void functions.

Automatically returning early in case of a failure in a sequence of function calls

As demonstrated above, the BOOST_NOEXCEPT_CHECK macro can be used to automate error checking when using function composition (calling one function with the result of another, as in f(g()).

While void function calls can not be composed, the BOOST_NOEXCEPT_CHECK_VOID macro could be used in a similar manner:

void f1() noexcept {
    BOOST_NOEXCEPT_CHECK_VOID
    ....
    if( !f1_succeeds )
        return (void) throw_( error1() );
}

void f2() noexcept {
    BOOST_NOEXCEPT_CHECK_VOID
    ....
    if( !f2_succeeds )
        return (void) throw_( error2() );
}

void f3() noexcept {
    BOOST_NOEXCEPT_CHECK_VOID
    ....
    if( !f3_succeeds )
        return (void) throw_( error3() );
}

void caller() noexcept {
    //No need to check for errors here, since f1, f2 and f3 each check for errors:
    f1();
    f2();
    f3();
}

This works, but as is the case when using function composition with non-void functions, all 3 functions will be called, even though they would return immediately in case the previous one failed.

Can Noexcept help avoid calling f2 or f3 if f1 fails? Yes it can, though we must refactor the void functions to return bool (indicating success or failure) which is a good idea anyway for any void function which could fail:

bool f1() noexcept {
    BOOST_NOEXCEPT_CHECK
    ....
    if( f1_succeeds )
        return true;
    else
        return throw_( error1() );
}

bool f2() noexcept {
    BOOST_NOEXCEPT_CHECK
    ....
    if( f2_succeeds )
        return true;
    else
        return throw_( error2() );
}

bool f3() noexcept {
    BOOST_NOEXCEPT_CHECK
    ....
    if( f3_succeeds )
        return true;
    else
        return throw_( error3() );
}

With this change, the caller function can simply use operator && to both propagate errors from any of f1, f2 or f3 and return early if any of them fails:

bool caller() noexcept {
    return
        f1() &&
        f2() &&
        f3();
}

Obtaining diagnostic information from unknown errors

Noexcept is compatible with boost::diagnostic_information() without being coupled with it.

Tip
boost::diagnostic_information does not require exception handling to be enabled.

One possible use case is to print diagnostic information about unhandled errors that reach the main function:

int main() {
    if( auto r=try_(do_work(....)) ) {
        return 0;
    } else if( auto err=r.catch_<error1>() ) {
        //print an error message about error1, then return to the OS
        return 1;
    } else if( auto err=r.catch_<error2>() ) {
        //print an error message about error2, then return to the OS
        return 2;
    } else {
        //Unknown error!
        auto err=r.catch_<>();
        assert(err!=0); //catch_<> will bind any error type
        std::cerr <<
            "Unhandled error reached main!\n"
            "Diagnostic information follows:\n" <<
            boost::diagnostic_information(*err);
        return 3;
    }
}

Calling boost::diagnostic_information will probe the passed object and extract as much useful information as possible. This includes the location the error was reported (if available: file, line number, function name), the output from std::exception::what(), as well as any boost::exception-style error info. An example output may look like this:

test/diagnostic_information_test.cpp(26): Throw in function int f1()
Dynamic exception type: boost::noexcept_::noexcept_detail::class_dispatch<my_error, true, false>::type
std::exception::what: my_error
[answer_*] = 42

Augmenting error objects in error-neutral contexts

Consider the following error type:

class file_read_error {
  std::string fn_;
  public:
  explicit file_read_error( std::string && fn ) noexcept:
    fn_(std::move(fn)) { }
  std::string const & file_name() const noexcept { return fn_; }
};

A call to catch_ that handles file_read_error:

if( auto e=r.catch_<file_read_error>() ) {
  std::cerr << "Error reading \"" << e->file_name() << "\"\n";
}

Finally, a function that may report a file_read_error using Noexcept:

bool read_file(FILE * f) noexcept {
  ....
  size_t nr=fread(buf,1,count,f);
  if(ferror(f))
    return throw_(file_read_error("???")); //File name not available here!
  ....
}

The issue is that the catch_ needs a file name, but at the point of the throw_ a file name is not available (only a FILE pointer is). In general the error might be detected in a library which can not assume that a meaningful name is available for any FILE it reads, even if a program that uses the library could reasonably make the same assumption.

Using boost::error_info a file name may be added to any error after it has been passed to throw_, while anything available at the point of the throw_ (e.g. errno) may be stored in the original object​:

class file_io_error: public boost::exception { };
class file_open_error: public virtual file_io_error { };
class file_read_error: public virtual file_io_error { };
typedef boost::error_info<struct xi_file_name_,std::string> xi_file_name;
typedef boost::error_info<struct xi_errno_,int> xi_errno;

using namespace boost::noexcept_;

bool read_file(FILE * f) noexcept {
    ....
    size_t nr=fread(buf,1,count,f);
    if(ferror(f))
        return throw_( file_read_error() << xi_errno(errno) );
    ....
}

bool process_file(char const * name) noexcept {
    if( FILE * fp=fopen(name,"rt") ) {
        std::shared_ptr<FILE> f(fp,fclose);

        if( auto r=try_(read_file(fp)) ) {
            //success!
            return true;
        } else if( auto err=r.catch_<boost::exception>() ) {
            //Augment any passing error:
            (*err) << xi_file_name(name);
            return r.throw_();
        }

    } else {

        //report failure to open the file:
        return throw_( file_open_error() << xi_file_name(name) );

    }
}

Now the final catch_ may look like this:

bool do_work() noexcept {
    return process_file("file.txt");
}

int main() {
    if( auto r=try_(do_work()) ) {

        std::cout << "Success!\n";
        return 0;

    } else if( auto err=r.catch_<file_io_error> {

        std::cerr << "I/O error!\n";
        std::string const * fn=boost::get_error_info<xi_file_name>();
        assert(fn!=0); //In this program all files have names.
        std::cerr << "File name: " << *fn << "\n";
        return 1;

    } else {

        //Unknown error!
        auto err=r.catch_<>();
        assert(err!=0); //catch_<> will bind any error type
        std::cerr <<
            "Unhandled error reached main!\n"
            "Diagnostic information follows:\n" <<
            boost::diagnostic_information(*err);
        return 2;

    }
}
Note
boost::error_info does not require exception handling. Noexcept is not coupled with boost::error_info.

Passing "value-or-error" between threads using result<T>

The usual Noexcept error handling semantics (throw_, try_, catch_) are a poor match for use cases where the value (or the error) can not be consumed immediately, for example when it is produced asynchronously in a worker thread, or when values are temporarily stored into a queue for later consumption, in which case the queue must have the ability to hold multiple outstanding errors.

The result<T> class template is designed for use in such cases. For example, suppose we have this function which simulates a computation which may succeed or fail:

int compute() noexcept {
    if( rand()%2 )
        return 42;
    else
        return throw_(compute_error());
}

As usual, it uses throw_ to report failures. Using std::future<result<int> > to collect the results from many concurrent calls to compute is easy:

std::vector<std::future<result<int> > > fut;

//Create and launch 100 tasks:
std::generate_n( std::inserter(fut,fut.end()), 100, [ ] {

    //Call compute() and capture the result into a result<int>:
    std::packaged_task<result<int>()> task( [ ] {
        return try_(compute());
    } );

    //Get the future and kick off the task
    auto f=task.get_future();
    std::thread(std::move(task)).detach();
    return f;

} );

We can now wait on each future then pass the collected result object to try_ to either get the successfully computed value or the failure, now transported into the main thread:

for( auto & f:fut ) {
    f.wait();
    if( auto r=try_(f.get()) )
        std::cout << "Success! Answer=" << r.get() << '\n';
    else if( auto err=r.catch_<compute_error>() )
        std::cout << "Failure!\n";
}

Alternatives to Noexcept

Besides Noexcept, there are multiple other C++ libraries and proposals for solving the problem of error reporting without using exceptions:

  • D0323R2 by Vicente J. Botet Escriba proposes a class template expected<T,E>, designed to be used as a return type in functions that could fail, the general idea being that in case of success the function returns a result of type T, or else it returns a reason for the failure, of type E.

  • variant2 by Peter Dimov is a never-valueless implementation of std::variant and an extended version of expected<T,E>. The class boost::variant2::expected<T,E…​> represents the return type of an operation that may potentially fail. It contains either the expected result of type T, or a reason for the failure, of one of the error types in E…​ (internally, this is stored as variant<T,E…​>).

  • Outcome by Niall Douglas recently underwent a formal Boost review. Unlike expected<T,E> and expected<T,E…​>, in outcome<T> the error is either of type std::error_code or an exception object, transported as a std::exception_ptr. The motivation for this more restrictive interface is to force the user to "be a good citizen" and communicate noexcept failures in terms of std::error_code, in order to facilitate interoperability between different libraries (with the added ability to capture exceptions in case some lower level library throws).

  • P0262 by Lawrence Crowl and Chris Mysen proposes a control helper called status_value<Status,Value>, which differs from all of the above libraries in that the returning function is expected to always provide a status and a value. This can be useful to communicate conditions which did not lead to a failure but are still likely to be of interest to the caller.

Macros and configuration

BOOST_NOEXCEPT_ASSERT

All assertions in Noexcept use this macro; if not #defined, Noexcept header files #define it as BOOST_ASSERT.


BOOST_NOEXCEPT_NO_THREADS

If #defined, Noexcept assumes that static storage is equivalent to thread-local storage.

If BOOST_NOEXCEPT_NO_THREADS is not explicitly #defined, thread-safety depends on BOOST_NO_THREADS.


BOOST_NOEXCEPT_THREAD_LOCAL(type,object)

This macro is used to define objects with static thread-local storage; if not #defined, Noexcept header files #define it as:

#define BOOST_NOEXCEPT_THREAD_LOCAL(type,object) static thread_local type object

or, under BOOST_NOEXCEPT_NO_THREADS, as:

#define BOOST_NOEXCEPT_THREAD_LOCAL(type,object) static type object

BOOST_NOEXCEPT_NO_RTTI

If #defined, Noexcept will not use dynamic_cast (and errors bound by catch_<T> () must match T exactly).

If BOOST_NOEXCEPT_NO_RTTI is not explicitly #defined, the availability of RTTI is determined using BOOST_NO_RTTI.


BOOST_NOEXCEPT_THROW_EXCEPTION

All Noexcept functions are declared as noexcept, except for result::get() which throws exceptions using this macro. If not #defined, Noexcept header files #define it as BOOST_THROW_EXCEPTION.


BOOST_NOEXCEPT_NO_EXCEPTION_INFO

While Noexcept is not coupled with boost::error_info, by default it is coupled with a tiny part of Boost which is needed to make boost::error_info work if used. Defining this macro removes that dependency.

Building and installation

Noexcept is a header-only library and requires no building or installation.

Building the unit tests and the examples

The unit tests and the examples can be built within the Boost framework: clone Noexcept under the libs subdirectory in your boost installation, then cd into noexcept/test and execute b2 as usual.

Benchmark

Error handling library benchmarks are tricky to write and with limited usefulness as design exploration tool, but since speed!!! is a hot topic of discussion, Noexcept includes a benchmark program called deep_stack. It can propagate different types of return values and error objects across configurable depth of function calls. In addition:

  • If exception handling is disabled, deep_stack uses Noexcept to propagate errors, otherwise it uses C++ exceptions.

  • By default inlining is disabled, which simulates propagating errors across different compilation units. If BOOST_NOEXCEPT_INLINE_FORCEINLINE is #defined, the function calls are inlined, which simulates propagating errors within a single compilation unit.

Below is the result of running deep_stack, compiled with Apple LLVM version 9.0.0 (clang-900.0.39.2), on an old-ish Macbook Pro, using a variety of compiler command lines (assuming the current working directory is ${BOOST_ROOT}/libs/noexcept/benchmark).

The source code of deep_stack is available here.

clang++ -O3 -I../include -I../../.. ./deep_stack.cpp -fno-exceptions
Using Noexcept (1)

void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = int], count=420000, success %=10: 14ms
void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = int], count=420000, success %=90: 9ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = int], count=420000, success %=10: 26ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = int], count=420000, success %=90: 22ms

void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=10: 20ms
void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=90: 16ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=10: 83ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=90: 76ms
  1. Exception handling disabled, inlining disabled, using Noexcept to propagate errors across 10 or 30 stack frames, with 10% or 90% chance of the operation being successful (vs. failing), returning int or string in case of success.

clang++ -O3 -I../include -I ../../.. deep_stack.cpp
Using C++ exception handling (1)

void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = int], count=420000, success %=10: 371ms
void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = int], count=420000, success %=90: 45ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = int], count=420000, success %=10: 397ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = int], count=420000, success %=90: 57ms

void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=10: 792ms
void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=90: 95ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=10: 1708ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=90: 239ms
  1. Exception handling enabled, inlining disabled, using throw to propagate errors across 10 or 30 stack frames, with 10% or 90% chance of the operation being successful (vs. failing), returning int or string in case of success.

clang++ -O3 -I../include -I ../../.. deep_stack.cpp -fno-exceptions -DBENCHMARK_INLINE=BOOST_NOEXCEPT_INLINE_FORCEINLINE
Using Noexcept (1)

void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = int], count=420000, success %=10: 11ms
void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = int], count=420000, success %=90: 6ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = int], count=420000, success %=10: 11ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = int], count=420000, success %=90: 7ms

void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=10: 11ms
void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=90: 8ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=10: 12ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=90: 7ms
  1. Exception handling disabled, inlining enabled, using Noexcept to propagate errors across 10 or 30 levels of inlined function calls, with 10% or 90% chance of the operation being successful (vs. failing), returning int or string in case of success.

clang++ -O3 -I../include -I ../../.. deep_stack.cpp -DBENCHMARK_INLINE=BOOST_NOEXCEPT_INLINE_FORCEINLINE
Using C++ exception handling (1)

void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = int], count=420000, success %=10: 339ms
void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = int], count=420000, success %=90: 38ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = int], count=420000, success %=10: 337ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = int], count=420000, success %=90: 39ms

void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=10: 334ms
void test_case(int, int) [Depth = 10, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=90: 41ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=10: 335ms
void test_case(int, int) [Depth = 30, ErrorType = my_error, ValueType = std::__1::basic_string<char>], count=420000, success %=90: 40ms
  1. Exception handling enabled, inlining enabled, using throw to propagate errors across 10 or 30 levels of inlined function calls, with 10% or 90% chance of the operation being successful (vs. failing), returning int or string in case of success.

Q&A

  1. Does Noexcept allocate memory dynamically?

    No.

  2. Does Noexcept require RTTI?

    No.

  3. When using exception handling, there can be multiple active exception objects in each thread. Does Noexcept support multiple active errors?

    Yes, the current error object can be captured using try_, at which point the calling thread is left without a current error.

    However, it is up to the user to call try_: the code below is ill-formed, assuming that both function calls may fail — that is, return using throw_(my_error()), for some user-defined type my_error:

    auto r1=f1();
    auto r2=f2();

    The correct code would ensure that at the time f2() is called there is no current error for the calling thread. This could be achieved either by checking the return value of f1() (assuming r1 converts to bool to indicate success or failure):

    if( auto r1=f1() ) {
        auto r2=f2(); //okay for f2 to fail (f1 didn't)
        ....
    } else {
        return r1.throw_(); //let the caller deal with failures
    }

    or by capturing f1 failures using try_:

    auto r1=try_(f1()); //capture possible f1 failures
    auto r2=f2(); //okay, even if both f1 and f2 fail
    Note
    The above is not unlike when using exception handling: if you want to call f2 even if f1 throws, you’d have to call f1 from within a try/catch.

    (Nothrow asserts on !has_current_error() at the time an error object is passed to throw_).

  4. Does this mean that I should always use try_?

    No, only use try_ if you want to handle errors (see catch_). In error-neutral contexts, in case of errors simply return throw_() without argument:

    if( auto r=f() ) {
        //Success -- use r
    } else {
        return r.throw_(); //Something went wrong
    }

    The above assumes that r converts to bool to indicate success or failure (there are many types than can be used this way). If that is not the case, ideally you would still be able to inspect the returned value to detect failures. If that is also not possible, use has_current_error().

  5. What happens if I forget to check for errors?

    Then you’d be using a bad value. For example, if a function returns a shared_ptr<T> and you forget to check for success, attempting to dereference it leads to undefined behavior (segfault).

    Note
    If control is entering a scope where exception handling is enabled, you can convert Noexcept errors to exceptions by try_(f()).get(), which throws on error.

    That said, it is sometimes possible to get away with not checking for errors in error-neutral contexts, see Programming techniques and guidelines.

  6. What happens if try_ is called without any error being present?

    That is fine, in this case the value passed to try_ will simply be moved into the returned result object, where it can be accessed using the get() member function.

  7. Has Noexcept been benchmarked?

    Not yet, but performance is an important design goal in Noexcept and I welcome any data or analysis contributions. That said, except in functions that handle errors (see try_), Noexcept has no effect on the speed of passing return values up the call chain.

  8. Doesn’t Noexcept make it too easy to forget to check for errors? For example, if a function that may fail returns a type without explicit empty state (like int), there is nothing to protect the user from ignoring errors by mistake!

    Noexcept does protect the user from this type of mistakes, but it can’t do it on the spot; see the No error gets ignored guarantee.

    If this does not suffice, don’t write functions that return int to indicate success or failure. However, consider that wrapping the int in a user-defined type adds complexity which may or may not be appropriate.

  9. Passing errors through thread-local storage doesn’t work if the consumption of the result of a function call (success or error) can not be consumed immediately. What if I need to postpone that until later?

    The result<T> template solves this exact problem. See Programming techniques and guidelines.

Acknowledgements

Noexcept was inspired by the discussions on the Boost Developers mailing list during the review of the Outcome library by Niall Douglas.

This second evolution of Noexcept incorporates valuable positive and negative feedback received after Noexcept was announced. Special thanks to Bjorn Reese and Peter Dimov for pointing out the previous inability of Noexcept to capture results into a result<T> with a hard value-or-error invariant, to Gavin Lambert for helping me discover that that throw_() was broken, and to Andrzej Krzemienski whose criticisms lead to improvements in the Noexcept mechanisms for writing error-neutral functions.


© 2017-2018 Emil Dotchevski