Simple macro for single-statement exception handling

Zoppo
CERTIFIED EXPERT
Published:
Edited by: Andrew Leniart
For a long time, I consider it very cumbersome to handle every possible exception in many function calls, especially in trivial cases where a return value could be used in a meaningful way even in the case of unexpected behaviour - Here I want to describe something I developed to make this easier.

As a developer, when using other people's code/libraries, you are forced to consider exceptions, and I think there are two major problems:

1. The developer needs to know if a foreign function can throw up an exception at all.
2. The code must be written differently accordingly (see first example below).

Problem 1 can only be solved if developers adhere to a certain discipline to search for clues in the documentation.

Regarding problem 2 I wanted to find a way to use exceptions as easily as possible, in a way that syntactically there is no difference to code that only uses return values.

A prime example of this problem is the function std::filesystem::exists. I've seen a lot of code from other developers where this function was used without catching a possible exception - apparently, not all developers know by far that if an exception can be thrown in this function, it throws filesystem_error on underlying OS API errors. I.e. if the passed path is a network path which cannot be found. 


IMHO it is a bad idea to be implemented this way and here's why. From my understanding a function named exists should return true if it exists, otherwise it should return false - this for me are the only two meaningful possibilities which I expect, usually I don't care about why it doesn't exist, I only want to know if or if not ... if I want to know why something doesn't exist I would expect another function, which can tell me why.


This means a function like this is dangerous and may cause a unhandled exception:


void foo( const std::string& file_name )
{
    if ( false == std::filesystem::exists( file_name ) )
    {
        return;
    }

    // do something ...
}

To make this function exception-safe it has to be written (or better said: I usually did it) somehow like this:


void foo( const std::string& file_name )
{
    try
    {
        if ( false == std::filesystem::exists( file_name ) )
        {
            return;
        }
    }
    catch (...)
    {
        return;
    }

    // do something ...
}

IMO this is quite a lot of additional code to handle something really trivial.


Some weeks ago I found some time to think about this, and after some testing, I got a way to handle exceptions with macros in a very easy way without the need to explicitly write the try/catch closure.


Because I am convinced that it makes it easier to write secure code, I decided to publish my solution here.


In the attached header safe_call.h (which I think requires C++ 14, I didn't test with C++ 11) I implemented two macros (SAFE_CALL and SAFE_CALL_RET) which are intended to be used with single function calls.


With this the above sample can be written in this way:


void foo( const std::string& file_name )
{
    if ( false == SAFE_CALL( std::filesystem::exists( file_name ) ) )
    {
        return;
    }

    // do something ...
}

The macro calls the passed function, if no exception occurs it returns whatever this call returned, but in case an exception is thrown in the called function, it returns a default constructed instance of the same type the called function returns.


This default constructed instance is created with {}, so there are many cases where this leads to senseful (and expected) results (like false for bool, 0 for numbers, nullptr for pointers, empty strings or containers, ...) - here two more samples:


void bar( const std::string& path )
{
     for ( const auto& file : SAFE_CALL( std::filesystem::directory_iterator( path ) ) )
     {
          // do somthing
     }
}

// this sample requires C++ 17
template < typename TYPE >
void dump_vector( const std::any& data )
{
     for ( const auto& d : SAFE_CALL( std::any_cast< std::vector< TYPE > >( data ) ) )
     {
          std::cout << d << '\n';
     }
}

In case the passed function returns void the macro returns a bool, which is false in case an exception was thrown in the called function, otherwise it returns true, i.e.:


void bar( const std::string& filename )
{
     if ( false == std::filesystem::exists( filename ) ) // may throw
     {
          return;
     }

     // do something
}


void foo( const std::string& filename )
{
     if ( false == SAFE_CALL( bar( filename ) ) )
     {
          return;
     }


     // do somthing
}

There may be cases, where it's meaningful, to return something different than a default constructed instance in case an exception occurred - this can be done using the macro SAFE_CALL_RET, it expects an instance of the same type as a second value, in which case an exception occurs and this instance is returned, i.e.:


void foo( const std::string& number )
{
     std::cout << "default: "<< SAFE_CALL( std::stoi( number ) ) << "\n";
     std::cout << "explicit: "<< SAFE_CALL_RET( std::stoi( number ), -1 ) << "\n";
}

int main( int argc, char* argv[] )
{
     foo( "9999999999" );
}

// Output:
// default: 0
// explicit: -1

For me, this is a very handy way to handle an exception in very many cases.


To keep the code I uploaded here simple it only handles std::exception or all exceptions (...), but this can be extended very easy in the function safe_call_exception_handler.


The caught exceptions are logged to std::clog including the source-file/line of the causing SAFE_CALL, which is very helpful to analyze problems (this behaviour can be changed in the functions log_exception) . I.e. with the above sample using std::any_cast something like this will be logged:


c:\projects\test\testexception\testexception.cpp(188): Bad any_cast


That's it - I hope you find this interesting and/or helpful.


Please tell me when you encounter any problems or want to discuss something about this issue - I appreciate all feedback.


Best regards,


ZOPPO



0
1,136 Views
Zoppo
CERTIFIED EXPERT

Comments (0)

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.