- Abstract
- Distribution
- Feedback / support
- Design rationale
- No error gets ignored guarantee
- Crash course
- Synopsis
- Reference
- Programming techniques and guidelines
- Selecting optimal function return types
- Enforcing successful control flow
- Automatically returning early in case of a failure in a sequence of function calls
- Obtaining diagnostic information from unknown errors
- Augmenting error objects in error-neutral contexts
- Passing "value-or-error" between threads using
result<T>
- Alternatives to Noexcept
- Macros and configuration
- Building and installation
- Building the unit tests and the examples
- Benchmark
- Q&A
- Acknowledgements
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.
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. |
Please use the Boost Developers mailing list.
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:
-
Zero abstraction penalty, at the very least in the case the function succeeds.
-
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. |
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
.
link:./examples/FILE_ptr_example.cpp[role=include]
-
If an error is detected,
throw_
associates the passedstd::error_code
object with the calling thread and converts to a0
FILE *
for the return value (as specified by thethrow_return
template). -
try_
checks for success and moves the return value ofopen_file
(or the error it passed tothrow_
) intor
. -
catch_
returns a pointer to the error object (or0
if it is not astd::error_code
) and flags it as handled. -
When
r
is destroyed, the error object is destroyed if handled, otherwise left intact for anothertry_
/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.
|
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]
-
open_stream
returns a "good"ifstream
orstd::error_code
(thethrow_
converts to an emptyifstream
, as specified by thethrow_return
template). -
The
BOOST_NOEXCEPT_CHECK
(on the previous line) would immediately return any pending errors to the caller usingreturn throw_()
, so here we canassert(f.good())
. -
The
read_line
function returns astring
or anotherstd::error_code
. -
Thanks to the
BOOST_NOEXCEPT_CHECK
inread_line
, here we don’t need to check for errors: any success or failure reported byread_line
oropen_stream
will be propagated automatically. -
Dispatch on known errors and print the appropriate message.
-
The commented-out section shows how
boost::diagnostic_information()
could be used by a more complex program to report thatmain()
could not recognize an error object that reached it.
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]
-
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-globaldo_work
function, which is a C++ function. -
In the C++
do_work
function, in case of success we return42
back to Lua. -
In case of a failure, we pass
do_work_error
tothrow_
as usual, and calllua_error
. -
The
call_lua
function instructs the Lua interpreter to execute thecall_do_work
Lua function (which callsdo_work
). Ifdo_work
was successful,call_lua
returns the answer; otherwise it forwards failures byreturn throw_()
, as usual. -
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.
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]
-
A C function which uses
int
status codes to indicate success or failure, and afloat
pointer to store the result. -
A simple error type to report failures from the C API function.
-
The cumbersome
float
pointer interface of theC
function is converted toboost::optional<float>
. Thethrow_
returns an emptyoptional
in case of failure (as specified by thethrow_return
template). -
try_
both checks for success and moves the return value oferratic_caller
intor
. -
catch_
returns a pointer to the error object (or0
if it can not be converted toerratic_error
) and flags it as handled.
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 notbool
andstd::is_integral<T>::value
istrue
, thenthrow_return<T>::value()
returnsstatic_cast<T>(-1)
. -
Otherwise it returns
T()
.
The throw_
class has the following members:
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:
- Effects:
-
e
becomes the current error for the calling thread. Iffile
,line
andfunction
are specified, that information is recorded for diagnostic purposes (use with theTHROW_
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.
|
template <class T>
operator T() noexcept;
- Preconditions:
- Returns:
-
throw_return<T>::value()
.
bool has_current_error() noexcept;
#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.
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()
isfalse
), use theget
member function of the returnedresult<T>
object to obtain the returned value. -
Otherwise instead of storing a
T
result, the returnedresult<T>
object captures the current error, which leaves the calling thread without a current error. Use thecatch_
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() noexcept;
-
At the time a
result<T>
object for whichhas_unhandled_error()
istrue
is destroyed:-
If
has_current_error()
is alsotrue
,result<T>
callsstd::terminate()
. -
Otherwise the error contained in
*this
becomes the current error for the calling thread again.
-
-
Otherwise (
has_unhandled_error()
isfalse
), ifhas_error()
istrue
, the captured error object is destroyed. -
Otherwise the captured
T
value is destroyed.
bool has_error() const noexcept;
- Returns:
-
true
if*this
contains an error object,false
if it contains a value.
bool has_unhandled_error() const noexcept;
- Returns:
-
true
ifhas_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 .
|
template <class E=std::exception>
E * catch_() noexcept;
- Returns:
-
If
has_error()
istrue
and the error object contained in*this
is of typeE
, returns a pointer to that object; otherwise returns0
. The returned pointer becomes invalid if theresult
object is moved or destroyed. - Effects:
-
The error object captured into
*this
is marked as handled but only if it is of typeE
; 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_ .
|
<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
}
void throw_exception();
- Preconditions:
-
has_error()
. - Effects:
-
Throws the error object as a C++ exception.
//Not available in result<void>:
T const & get() const;
T & get();
- Effects:
-
If
!has_error()
,get()
returns a reference to the object passed totry_
and moved into*this
; otherwiseget()
callsthrow_exception()
.
template <class T>
result<T> try_( T && res ) noexcept;
result<void> void_try_() noexcept;
- Effects:
-
-
If
!has_current_error()
,res
is moved into the returnedresult<T>
object, later accessible by a call toget()
. -
Otherwise the current error for the calling thread is moved into the returned
result<T>
object, later accessible by a call tocatch_()
.
-
- 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.
|
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 *
(notablyFILE *
) -
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> >
.
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.
|
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();
}
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
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 .
|
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";
}
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 typeT
, or else it returns a reason for the failure, of typeE
. -
variant2 by Peter Dimov is a never-valueless implementation of
std::variant
and an extended version ofexpected<T,E>
. The classboost::variant2::expected<T,E…>
represents the return type of an operation that may potentially fail. It contains either the expected result of typeT
, or a reason for the failure, of one of the error types inE…
(internally, this is stored asvariant<T,E…>
). -
Outcome by Niall Douglas recently underwent a formal Boost review. Unlike
expected<T,E>
andexpected<T,E…>
, inoutcome<T>
the error is either of typestd::error_code
or an exception object, transported as astd::exception_ptr
. The motivation for this more restrictive interface is to force the user to "be a good citizen" and communicatenoexcept
failures in terms ofstd::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.
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.
Noexcept is a header-only library and requires no building or installation.
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.
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.
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
-
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
orstring
in case of success.
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
-
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), returningint
orstring
in case of success.
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
-
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
orstring
in case of success.
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
-
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), returningint
orstring
in case of success.
-
Does Noexcept allocate memory dynamically?
No.
-
Does Noexcept require RTTI?
No.
-
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 usingthrow_(my_error())
, for some user-defined typemy_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 off1()
(assumingr1
converts tobool
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 usingtry_
:auto r1=try_(f1()); //capture possible f1 failures auto r2=f2(); //okay, even if both f1 and f2 fail
NoteThe above is not unlike when using exception handling: if you want to call f2
even iff1
throws, you’d have to callf1
from within atry
/catch
.(Nothrow asserts on
!has_current_error()
at the time an error object is passed tothrow_
). -
Does this mean that I should always use
try_
?No, only use
try_
if you want to handle errors (seecatch_
). In error-neutral contexts, in case of errors simply returnthrow_()
without argument:if( auto r=f() ) { //Success -- use r } else { return r.throw_(); //Something went wrong }
The above assumes that
r
converts tobool
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, usehas_current_error()
. -
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).NoteIf 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.
-
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 returnedresult
object, where it can be accessed using theget()
member function. -
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. -
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 theint
in a user-defined type adds complexity which may or may not be appropriate. -
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.
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