...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
Traditionally, when using exceptions to report failures, the throw site:
A higher context in the program contains a catch statement which:
The main issue with this "traditional" approach is that often, the data available at the point of the throw is insufficient for the catch site to handle the failure.
Here is an example of a catch statement:
catch( file_read_error & e ) { std::cerr << e.file_name(); }
And here is a possible matching throw:
void read_file( FILE * f ) { .... size_t nr=fread(buf,1,count,f); if( ferror(f) ) throw file_read_error(???); .... }
Clearly, the problem is that the handler requires a file name but the read_file function does not have a file name to put in the exception object; all it has is a FILE pointer!
In an attempt to deal with this problem, we could modify read_file to accept a file name:
void read_file( FILE * f, char const * name ) { .... size_t nr=fread(buf,1,count,f); if( ferror(f) ) throw file_read_error(name); .... }
This is not a real solution: it simply shifts the burden of supplying a file name to the immediate caller of the read_file function.
In general, the data required to handle a given library-emitted exception depends on the program that links to it. Many contexts between the throw and the catch may have relevant information which must be transported to the exception handler.
The idea of exception wrapping is to catch an exception from a lower level function (such as the read_file function above), and throw a new exception object that contains the original exception (and also carries a file name.) This method seems to be particularly popular with C++ programmers with Java background.
Exception wrapping leads to the following problems:
The second point is actually special case of violating the exception neutrality principle. Most contexts in a program can not handle exceptions; such contexts should not interfere with the process of exception handling.
For example, in the throw statement below we only add the errno code, since this is the only failure-relevant information available in this context:
struct exception_base: virtual std::exception, virtual boost::exception { }; struct io_error: virtual exception_base { }; struct file_read_error: virtual io_error { }; typedef boost::error_info<struct tag_errno_code,int> errno_code; void read_file( FILE * f ) { .... size_t nr=fread(buf,1,count,f); if( ferror(f) ) throw file_read_error() << errno_code(errno); .... }
In a higher exception-neutral context, we add the file name to any exception that derives from boost::exception:
typedef boost::error_info<struct tag_file_name,std::string> file_name; .... try { if( FILE * fp=fopen("foo.txt","rt") ) { shared_ptr<FILE> f(fp,fclose); .... read_file(fp); //throws types deriving from boost::exception do_something(); .... } else throw file_open_error() << errno_code(errno); } catch( boost::exception & e ) { e << file_name("foo.txt"); throw; }
Finally here is how the handler retrieves data from exceptions that derive from boost::exception:
catch( io_error & e ) { std::cerr << "I/O Error!\n"; if( std::string const * fn=get_error_info<file_name>(e) ) std::cerr << "File name: " << *fn << "\n"; if( int const * c=get_error_info<errno_code>(e) ) std::cerr << "OS says: " << strerror(*c) << "\n"; }
In addition, boost::diagnostic_information can be used to compose an automatic (if not user-friendly) message that contains all of the error_info objects added to a boost::exception. This is useful for inclusion in logs and other diagnostic objects.