🚀 UllrichLumina

Do you really write exception safe code closed

Do you really write exception safe code closed

📅 | 📂 Category: C++

Writing truly exception-safe code is a critical, yet often overlooked, aspect of robust software development. It’s more than just slapping a try-catch block around your code; it’s about anticipating and gracefully handling every possible error scenario to prevent data corruption, resource leaks, and unexpected program termination. How often do you consider what happens when a network connection drops mid-operation? Or when a file you’re writing to suddenly becomes inaccessible? Mastering exception safety separates good programmers from great ones, ensuring your applications remain stable and reliable even under duress.

Understanding Exception Safety Guarantees

Exception safety isn’t a binary concept; there are different levels of guarantee you can provide. Understanding these levels is crucial for designing and implementing robust software. The most common guarantees are the basic guarantee, the strong guarantee, and the no-throw guarantee. The basic guarantee ensures that if an exception is thrown, no resources are leaked, and the program remains in a valid, albeit potentially altered, state. The strong guarantee, however, goes further, ensuring that if an exception is thrown, the program’s state remains unchanged as if the operation never started. Finally, the no-throw guarantee ensures the operation will never throw an exception.

Choosing the appropriate guarantee depends on the specific operation and its context. While the no-throw guarantee is ideal, it’s often difficult to achieve in practice. The strong guarantee is desirable for complex operations, while the basic guarantee may suffice for simpler ones.

Techniques for Writing Exception-Safe Code

Achieving exception safety requires a disciplined approach and careful consideration of potential error conditions. One common technique is the Resource Acquisition Is Initialization (RAII) idiom, which ties resource management to object lifetimes. When an object goes out of scope, its destructor is automatically called, releasing any held resources. This ensures proper cleanup even in the presence of exceptions.

Another crucial technique is the Copy-and-Swap idiom. This involves creating a temporary copy of the object being modified, performing the operation on the copy, and then swapping the contents of the copy with the original object. If an exception occurs during the operation on the copy, the original object remains untouched.

  • Utilize RAII for automatic resource management.
  • Employ the Copy-and-Swap idiom for complex operations.

Handling Exceptions in C++

C++ provides a robust mechanism for handling exceptions through the try, catch, and throw keywords. Enclosing potentially risky code within a try block allows you to catch and handle exceptions thrown within that block. The catch blocks specify the types of exceptions to handle, and the throw keyword is used to raise exceptions. Effective exception handling requires catching specific exceptions rather than relying on generic catch(…) blocks. This allows for targeted handling of different error scenarios.

Consider the scenario of writing data to a file. A file stream operation could throw several exceptions, like std::ios_base::failure. Catching this specific exception provides valuable information about the error and allows for specific recovery actions, such as retrying the operation or logging the error. Blindly catching all exceptions can mask critical errors and make debugging more difficult.

  1. Enclose risky code within a try block.
  2. Use specific catch blocks for different exception types.
  3. Avoid generic catch(…) blocks unless absolutely necessary.

Practical Examples and Case Studies

Consider a database transaction. Imagine transferring funds between two accounts. If an exception occurs mid-transaction, you risk leaving the database in an inconsistent state. Exception safety ensures that either the entire transaction completes successfully, or the database is rolled back to its original state, preventing data corruption. This is often achieved using the strong exception guarantee.

Another example is a network application. If a connection drops during a data transfer, exception handling can prevent data loss and ensure the application gracefully handles the disconnection. For instance, the application could attempt to reconnect or inform the user of the network error. In such cases, the basic exception guarantee might be sufficient.

“Exception handling is not about recovering from errors; it’s about preventing failures.” - Bjarne Stroustrup

Learn more about advanced exception handling techniques.Best Practices for Exception Safety

Beyond the core techniques, several best practices can further enhance the exception safety of your code. Minimizing the scope of try blocks helps isolate potential exceptions, making them easier to handle. Never throw exceptions from destructors, as this can lead to unpredictable behavior, especially during stack unwinding. Documenting the exception guarantees provided by your functions clarifies their behavior and helps other developers use them correctly. Adhering to these practices contributes to building robust and reliable software.

  • Minimize the scope of try blocks.
  • Never throw exceptions from destructors.
  • Document the exception guarantees of your functions.

Implementing proper exception handling isn’t just a good practice—it’s a necessity for creating robust and reliable software. By understanding and applying the techniques and best practices outlined in this article, you can significantly improve the quality and stability of your code.

FAQ

Q: What is the difference between error handling and exception handling?

A: Error handling refers to the general process of dealing with errors in a program, while exception handling is a specific mechanism for handling exceptional conditions or errors that disrupt the normal flow of execution.

By proactively addressing potential issues and implementing robust exception handling mechanisms, you contribute to building more resilient and user-friendly applications. Explore resources like cppreference and ISO C++ FAQ to deepen your understanding and further refine your exception handling skills. Don’t just write code that works—write code that endures. Start prioritizing exception safety in your projects today and witness the transformative impact it has on the quality and reliability of your software. Also, consider exploring further Boost libraries for advanced exception handling functionalities.

Question & Answer :

Exception handling (EH) seems to be the current standard, and by searching the web, I can not find any novel ideas or methods that try to improve or replace it (well, some variations exist, but nothing novel).

Though most people seem to ignore it or just accept it, EH has some huge drawbacks: exceptions are invisible to the code and it creates many, many possible exit points. Joel on software wrote an article about it. The comparison to goto fits perfect, it made me think again about EH.

I try to avoid EH and just use return values, callbacks or whatever fits the purpose. But when you have to write reliable code, you just can’t ignore EH these days: It starts with the new, which may throw an exception, instead of just returning 0 (like in the old days). This makes about any line of C++ code vulnerable to an exception. And then more places in the C++ foundational code throw exceptions… std lib does it, and so on.

This feels like walking on shaky grounds.. So, now we are forced to take care about exceptions!

But its hard, its really hard. You have to learn to write exception safe code, and even if you have some experience with it, it will still be required to double check any single line of code to be safe! Or you start to put try/catch blocks everywhere, which clutters the code until it reaches a state of unreadability.

EH replaced the old clean deterministical approach (return values..), which had just a few but understandable and easily solveable drawbacks with an approach that creates many possible exit points in your code, and if you start writing code that catches exceptions (what you are forced to do at some point), then it even creates a multitude of paths through your code (code in the catch blocks, think about a server program where you need logging facilities other than std::cerr ..). EH has advantages, but that’s not the point.

My actual questions:

  • Do you really write exception safe code?
  • Are you sure your last “production ready” code is exception safe?
  • Can you even be sure, that it is?
  • Do you know and/or actually use alternatives that work?

Your question makes an assertion, that “Writing exception-safe code is very hard”. I will answer your questions first, and then, answer the hidden question behind them.

Answering questions

Do you really write exception safe code?

Of course, I do.

This is the reason Java lost a lot of its appeal to me as a C++ programmer (lack of RAII semantics), but I am digressing: This is a C++ question.

It is, in fact, necessary when you need to work with STL or Boost code. For example, C++ threads (boost::thread or std::thread) will throw an exception to exit gracefully.

Are you sure your last “production ready” code is exception safe?

Can you even be sure, that it is?

Writing exception-safe code is like writing bug-free code.

You can’t be 100% sure your code is exception safe. But then, you strive for it, using well-known patterns, and avoiding well-known anti-patterns.

Do you know and/or actually use alternatives that work?

There are no viable alternatives in C++ (i.e. you’ll need to revert back to C and avoid C++ libraries, as well as external surprises like Windows SEH).

Writing exception safe code

To write exception safe code, you must know first what level of exception safety each instruction you write is.

For example, a new can throw an exception, but assigning a built-in (e.g. an int, or a pointer) won’t fail. A swap will never fail (don’t ever write a throwing swap), a std::list::push_back can throw…

Exception guarantee

The first thing to understand is that you must be able to evaluate the exception guarantee offered by all of your functions:

  1. none: Your code should never offer that. This code will leak everything, and break down at the very first exception thrown.
  2. basic: This is the guarantee you must at the very least offer, that is, if an exception is thrown, no resources are leaked, and all objects are still whole
  3. strong: The processing will either succeed, or throw an exception, but if it throws, then the data will be in the same state as if the processing had not started at all (this gives a transactional power to C++)
  4. nothrow/nofail: The processing will succeed.

Example of code

The following code seems like correct C++, but in truth, offers the “none” guarantee, and thus, it is not correct:

void doSomething(T & t) { if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail t.integer += 1 ; // 1'. nothrow/nofail X * x = new X() ; // 2. basic : can throw with new and X constructor t.list.push_back(x) ; // 3. strong : can throw x->doSomethingThatCanThrow() ; // 4. basic : can throw } 

I write all my code with this kind of analysis in mind.

The lowest guarantee offered is basic, but then, the ordering of each instruction makes the whole function “none”, because if 3. throws, x will leak.

The first thing to do would be to make the function “basic”, that is putting x in a smart pointer until it is safely owned by the list:

void doSomething(T & t) { if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail t.integer += 1 ; // 1'. nothrow/nofail std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor X * px = x.get() ; // 2'. nothrow/nofail t.list.push_back(px) ; // 3. strong : can throw x.release() ; // 3'. nothrow/nofail px->doSomethingThatCanThrow() ; // 4. basic : can throw } 

Now, our code offers a “basic” guarantee. Nothing will leak, and all objects will be in a correct state. But we could offer more, that is, the strong guarantee. This is where it can become costly, and this is why not all C++ code is strong. Let’s try it:

void doSomething(T & t) { // we create "x" std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor X * px = x.get() ; // 2. nothrow/nofail px->doSomethingThatCanThrow() ; // 3. basic : can throw // we copy the original container to avoid changing it T t2(t) ; // 4. strong : can throw with T copy-constructor // we put "x" in the copied container t2.list.push_back(px) ; // 5. strong : can throw x.release() ; // 6. nothrow/nofail if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail t2.integer += 1 ; // 7'. nothrow/nofail // we swap both containers t.swap(t2) ; // 8. nothrow/nofail } 

We re-ordered the operations, first creating and setting X to its right value. If any operation fails, then t is not modified, so, operation 1 to 3 can be considered “strong”: If something throws, t is not modified, and X will not leak because it’s owned by the smart pointer.

Then, we create a copy t2 of t, and work on this copy from operation 4 to 7. If something throws, t2 is modified, but then, t is still the original. We still offer the strong guarantee.

Then, we swap t and t2. Swap operations should be nothrow in C++, so let’s hope the swap you wrote for T is nothrow (if it isn’t, rewrite it so it is nothrow).

So, if we reach the end of the function, everything succeeded (No need of a return type) and t has its excepted value. If it fails, then t has still its original value.

Now, offering the strong guarantee could be quite costly, so don’t strive to offer the strong guarantee to all your code, but if you can do it without a cost (and C++ inlining and other optimization could make all the code above costless), then do it. The function user will thank you for it.

Conclusion

It takes some habit to write exception-safe code. You’ll need to evaluate the guarantee offered by each instruction you’ll use, and then, you’ll need to evaluate the guarantee offered by a list of instructions.

Of course, the C++ compiler won’t back up the guarantee (in my code, I offer the guarantee as a @warning doxygen tag), which is kinda sad, but it should not stop you from trying to write exception-safe code.

Normal failure vs. bug

How can a programmer guarantee that a no-fail function will always succeed? After all, the function could have a bug.

This is true. The exception guarantees are supposed to be offered by bug-free code. But then, in any language, calling a function supposes the function is bug-free. No sane code protects itself against the possibility of it having a bug. Write code the best you can, and then, offer the guarantee with the supposition it is bug-free. And if there is a bug, correct it.

Exceptions are for exceptional processing failure, not for code bugs.

Last words

Now, the question is “Is this worth it ?”.

Of course, it is. Having a “nothrow/no-fail” function knowing that the function won’t fail is a great boon. The same can be said for a “strong” function, which enables you to write code with transactional semantics, like databases, with commit/rollback features, the commit being the normal execution of the code, throwing exceptions being the rollback.

Then, the “basic” is the very least guarantee you should offer. C++ is a very strong language there, with its scopes, enabling you to avoid any resource leaks (something a garbage collector would find it difficult to offer for the database, connection or file handles).

So, as far as I see it, it is worth it.

Edit 2010-01-29: About non-throwing swap

nobar made a comment that I believe, is quite relevant, because it is part of “how do you write exception safe code”:

  • [me] A swap will never fail (don’t even write a throwing swap)
  • [nobar] This is a good recommendation for custom-written swap() functions. It should be noted, however, that std::swap() can fail based on the operations that it uses internally

the default std::swap will make copies and assignments, which, for some objects, can throw. Thus, the default swap could throw, either used for your classes or even for STL classes. As far as the C++ standard is concerned, the swap operation for vector, deque, and list won’t throw, whereas it could for map if the comparison functor can throw on copy construction (See The C++ Programming Language, Special Edition, appendix E, E.4.3.Swap).

Looking at Visual C++ 2008 implementation of the vector’s swap, the vector’s swap won’t throw if the two vectors have the same allocator (i.e., the normal case), but will make copies if they have different allocators. And thus, I assume it could throw in this last case.

So, the original text still holds: Don’t ever write a throwing swap, but nobar’s comment must be remembered: Be sure the objects you’re swapping have a non-throwing swap.

Edit 2011-11-06: Interesting article

Dave Abrahams, who gave us the basic/strong/nothrow guarantees, described in an article his experience about making the STL exception safe:

http://www.boost.org/community/exception_safety.html

Look at the 7th point (Automated testing for exception-safety), where he relies on automated unit testing to make sure every case is tested. I guess this part is an excellent answer to the question author’s “Can you even be sure, that it is?”.

Edit 2013-05-31: Comment from dionadar

t.integer += 1; is without the guarantee that overflow will not happen NOT exception safe, and in fact may technically invoke UB! (Signed overflow is UB: C++11 5/4 “If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.”) Note that unsigned integer do not overflow, but do their computations in an equivalence class modulo 2^#bits.

Dionadar is referring to the following line, which indeed has undefined behaviour.

t.integer += 1 ; // 1. nothrow/nofail 

The solution here is to verify if the integer is already at its max value (using std::numeric_limits<T>::max()) before doing the addition.

My error would go in the “Normal failure vs. bug” section, that is, a bug. It doesn’t invalidate the reasoning, and it does not mean the exception-safe code is useless because impossible to attain. You can’t protect yourself against the computer switching off, or compiler bugs, or even your bugs, or other errors. You can’t attain perfection, but you can try to get as near as possible.

I corrected the code with Dionadar’s comment in mind.

🏷️ Tags: