Exception-safe code

During the last years the manner we handle exceptions has fundamentally changed. If we look few years back we will find a lot of applications with exception handling in bigger context only. For example, a module executing a bigger task may be executed in an own thread or process and exceptions according this module were cached and the module was restarted after an error. Nowadays the exception handling has moved to smaller parts of the application. Ideally, we will work with exception-safe methods now.

Within this article I want to show the base ideas behind exception handling on method level and think about the according programming concepts. I will show general development patterns and don’t want to explain implementation details like the try-catch syntax.

 

Typical issues

At first, we should think about typical issues occurring as result of erroneous execution of a method and implement an example application containing features according these issues.

If an error occurs the execution of a method will be interrupted. Often, several data objects will be updated within a method. If execution is interrupted and only a part of the data is changed we will have invalid or corrupted data.

The method interruption may also result in resource management issues. If a resource is created at the beginning on the execution and released at the end, an interruption will have the side effect that the resource is not released at all. Such a resource may be for example memory, a file, a database connection or a locking object. Therefore, we could see long term issues, like continuously rising memory workload and immediately occurring issues like an application freeze as result of a deadlock.

Within the example application I want to address the two common issues we identified so far: data corruption and resource management. As resource management is a broad topic I want to add different concerns: a database resource and a synchronization resource for multithreading.

The example application should manage a list of customers. Each customer has a name and a list of orders. The orders are stored within a database. A management class should allow a thread-safe access to the customer data objects. To keep it simple we will omit implementation details of the single components, e.g. the database access, and look at the exception-safe topics. So, we want to implement a single method only: adding a new customer. During the article, we will try to implement this method in an exception-safe manner.

The following source code shows the base implementation of the example application.

class Orders;
class DatabaseConnection;
class OrderFactory;

struct Customer
{
  unsigned int mIdentifier;
  std::string mName;
  Orders* pOrders;
};

class CustomerManagement
{
public:
  CustomerManagement() : mNextAvailableIdentifier(1) {};

  void AddCustomer(std::string name);

private:
  std::mutex mMutex;
  unsigned int mNextAvailableIdentifier;

  std::vector<Customer> mCustomers;
};

int _tmain(int argc, _TCHAR* argv[])
{
  CustomerManagement manager = CustomerManagement();

  manager.AddCustomer("John Doe");

	return 0;
}

 

The customer manager has some private members: a list of customers, a mutex to implement a thread-safe access to the customer list and a counter for the customer identifier. Each customer should get a unique identifier so we use an internal counter which is used for this purpose. I know, there are better solutions to implement such an identifier but this simple solution will help to show issues like data corruption on exceptions.

The three forward declarations contain functionalities we want to use but like explained before, we don’t want to implement these classes. The orders class is a data class to store an order, the database connection class will allow the access to a database and the order factory creates a new order list and stores it into the database.

 

First implementation

Now we want to implement the “AddCustomer” method. At first, we don’t think about exception safety and implement the method in a straightforward manner: lock, get resources, update data, release resources and unlock.

void CustomerManagement::AddCustomer(std::string name)
{
  mMutex.lock();

  Customer customer = Customer();
  customer.mIdentifier = mNextAvailableIdentifier;
  customer.mName = name;
  mCustomers.push_back(customer);

  DatabaseConnection* pConnection = new DatabaseConnection();
  mCustomers.back().pOrders = OrderFactory.CreateEmptyCollection(pConnection);
  delete pConnection;

  mNextAvailableIdentifier++;

  mMutex.unlock();
}

 

I am sure you have seen such method patterns quite often. But if we think about exception-safety we will identify some critical issues. What may happen if the method gets interrupted due to an exception, e.g. during a database access? Before you continue with reading think about this possibility and the issues which may occur.

I think we can identify three issues: a deadlock occurs as unlock will not be executed, the database workload increases as the database connection will not be closed and the internal data may be corrupted as the customer is not completely created or the internal counter for the identifier is not increased.

 

Requirements for exception safety

There are two common requirements for exception safety: leak no resources and don’t allow data structures to become corrupted. The deadlock issue belongs to the first topic as it is about management of a synchronization resource. So, we don’t add thread-safety as own requirement. At next we want to think about the two requirements and change the implementation of the “AddCustomer” method accordingly.

 

Leak no resources

Resource leaking may result in undefined behavior and can cause serious errors and difficult to reproduce strange behaviors of the application. But fortunately, you can avoid resource leaks in an amazingly simple way: use resource management objects.

The C++ language itself will ensure that all object instances created in the context of the method will be released at the end of the method execution. And this is independent whether we have a normal execution or a premature interruption due to an exception.

Within the example we use two resources: the locking object and the database connection object. We can use already existing implementations to instantiate resource management objects for these resources. The following code shows an implementation of the “AddCustomer” method with respect to the “leak no resources” requirement.

void CustomerManagement::AddCustomer(std::string name)
{
  std::lock_guard<std::mutex> guard(mMutex);
  
  Customer customer = Customer();
  customer.mIdentifier = mNextAvailableIdentifier;
  customer.mName = name;
  mCustomers.push_back(customer);

  std::unique_ptr<DatabaseConnection> pConnection(new DatabaseConnection());    
  mCustomers.back().pOrders = OrderFactory.CreateEmptyCollection(pConnection);  

  mNextAvailableIdentifier++;  
}

 

Don’t allow data structures to become corrupted

If you are looking for implementation patterns avoiding data corruption, you will find several solutions. But they (nearly) all following two basic ideas and can therefore grouped together. One group will ensure that the objects remains in a valid state. The data may not be correct, for example it may be initialized incomplete, but the object state is fine and the data structures itself are not corrupted. In such cases the client can decide whether he wants to undo the erroneous step or not. The second group of methods have an atomic behavior. They succeed completely or if they fail the data and application state is like before the function call.

At next I want to show the concept of exception-safety guarantees. This is a well-known and often used concept which is based on the previous described groups of data handling concepts. It extends these two concepts by a third one which adds methods which will never throw any exceptions.

 

Exception-safe guarantees

This programming concept says that each single method must implement one out of three exceptions-safe guarantees.

  • Basic guarantee
  • Strong guarantee
  • No-throw guarantee

Within the next sections I want to explain each of the three guarantees and change the example method according these concepts.

 

Basic guarantee

A method which gives the basic guarantee will ensure that everything remains on a valid state and that there is no corrupted data. But the precise program state may not be predictable. Therefore, based on the type of error or moment when it occurs, the data and program state may be different on two function calls, but the data and state are always valid and not corrupted. The client is responsible to handle errors and may clean up data and repeat the method call if necessary.

The following source code shows the adapted “AddCustomer” method, which will give the basic guarantee now.

void CustomerManagement::AddCustomer(std::string name)
{
  std::lock_guard<std::mutex> guard(mMutex);

  std::unique_ptr<DatabaseConnection> pConnection(new DatabaseConnection());

  Customer customer = Customer();
  customer.mIdentifier = mNextAvailableIdentifier;
  customer.mName = name;
  customer.pOrders = nullptr;

  mCustomers.push_back(customer);
  mNextAvailableIdentifier++;

  mCustomers.back().pOrders = OrderFactory.CreateEmptyCollection(pConnection);
}

 

Let us think about this implementation. What can happen in case of exceptions?

Locking and database access is implemented by using resource management objects. Independent at which moment an error occurs, the objects will be released and stay valid. So, at next, let us have a look at the data structures. The internal data structure contains a list with customers and a counter to create the customer identifier. We have two sub-function calls which potentially may throw an error: creating the database connection and create the empty order collection. Independent whether the one or the other throws an error, the data structure remains valid but it will have different states. If the database creation fails, the method call will return with an error. In this case no customer was added at all and the internal counter for the identifier creation was not changed. If the second sub-method call fails, the customer was already added with standard values (null pointer for orders list) and the internal counter for the identifier creation was changed too. So, the data structure is valid but it has a state different from the first error case.

Of course, our customer management object will offer some more functions, like searching for a customer and delete a customer. Therefore, after an error the client will be able to check the actual object state and continue accordingly.

Methods offering the “basic guarantee” will ensure a valid objects state and data which is not corrupted but it will not ensure a defined state or data content after an exception.

 

Strong guarantee

The strong guarantee eliminates the disadvantage of the undefined state of the basic guarantee. A method giving the strong guarantee will always have one of two defined states: it is executed completely or it has the same state like before the execution. Therefore, such methods have an atomic or transactional behavior. If an error occurs the state and data of the object is unchanged. Everything remains in the same state as it was before. If the method succeeds it succeeds completely. In case of an error the client does not longer have to do any analysis of the object state and maybe clean up some data.

The following code shows the adapted implementation of the “AddCustomer” method.

void CustomerManagement::AddCustomer(std::string name)
{
  std::lock_guard<std::mutex> guard(mMutex);

  Customer customer = Customer();
  customer.mIdentifier = mNextAvailableIdentifier;
  customer.mName = name;
  customer.pOrders = nullptr;

  std::unique_ptr<DatabaseConnection> pConnection(new DatabaseConnection());
  customer.pOrders = OrderFactory.CreateEmptyCollection(pConnection);
  
  mCustomers.push_back(customer);
  mNextAvailableIdentifier++;    
}

 

Like in this example method, the strong behavior is often implemented by using temporary data objects. At the end of the method, if no error occurred, the actual object data and the temporary one will be swapped.

 

No-throw guarantee

A method giving this guarantee promise to never throw an application exception. It will always do what it promises to do and throw serious errors only, like an out of memory exception. For example, all operations on built-in types offer the no-throw guarantee.

If we try to implement our example method according this guarantee we will see two issues: the sub-functions call to open the database connection and the sub-function call to create the order list. With this design limitation, it is very difficult to implement a no-throw method. But, for example, we can implement a queue for database commands. This queue will get a command and returns immediately without any error. The command itself will be executed by the queue manager later. With such a design change our “AssCustomer” method will use sub-functions which give a no-throw guarantee and therefore we are able to give this guarantee too.

class DatabaseQueryQueue;

void CustomerManagement::AddCustomer(std::string name)
{
  std::lock_guard<std::mutex> guard(mMutex);

  Customer customer = Customer();
  customer.mIdentifier = mNextAvailableIdentifier;
  customer.mName = name;
  customer.pOrders = DatabaseQueryQueue.CreateEmptyCollection();

  mCustomers.push_back(customer);
  mNextAvailableIdentifier++;
}

 

Now we can offer the no-throw guarantee. But the needed software design change is expensive. We must implement a new manager layer to access the database and adapt all our existing code accordingly. This brings us to the question which of the three guarantees we should offer.

 

Choose between the three guarantees

Exception safe code must offer one of the three guarantees. In my opinion, any of the functions you write should be exception-safe. Resource management should be done by management objects anyway and the data access should be done in a logical manner. Therefore, if you follow some base programming guidelines your methods already should give the basis guarantee, or only some little modifications are necessary.

If a method and therefore the application using this method is not exception-safe, it can result in resource leaks or corrupt data which results in unexpected application behavior and errors. You don’t want this and therefore exception safety is a basic need for your code.

Which guarantee is given by a function should be an individual choice. You can compare risk of exceptions and costs of exception safe implementations. No-throw methods are wonderful from a client point of view but they may be very expensive. If the implementation effort for different kinds of the three guarantees is nearly the same, you should choose the strongest one. But this guideline may be wrong in some situation as a stronger guarantee may not be practical in 100% of the time. For example, the strong guarantee often uses temporary objects or store the previous state and therefore it must create additional objects and must executed additional copy or move commands. This kind of exceptions-safety guarantee may not be reasonable for time critical parts of your application.

In summary, the choice of the suitable guarantee depends on the requirements of the application, the module, the object and the single method. These requirements normally limit the decision to one or two of the three guarantees. For the remaining ones, you must balance benefit against implementation effort to make your decision.

 

Sub-function calls

As seen in our example method, often we depend on existing functions which we want to use in our method. If we call a function within our method we are limited to their exception guarantee level and cannot give a higher one. If we call several methods the guarantee level may even be reduced. For example, we call two functions giving a strong guarantee. If the second function fails the changes of the first have already be done. Therefore, we can give the basic guarantee only.

You should keep this in mind in case you want to estimate the costs of an implementation. The components you are using will limit the safety guarantee you are able to give. If you must give a higher guarantee you may not be able to use the existing component and for example implement an exception safe proxy for the sub-component. This will have a great impact on the implementation costs of you component.

 

Exception-safe application

The exception-safety of the whole application depends on the guarantees the single functions can give. Even one function without exception safety will make the whole application unsafe, because in case that single function throws an error the whole application is in an undefined and unsafe state. The same is true for an object or module. The exception-safe guarantee of the whole software or a software component is according to the lowest guarantee of all their functions.

 

Summary

It is not difficult to write exception safe code. You simply must pay attention to two concepts. At first use objects to manage resources. This will prevent resource leaks. And at second think about the three safety-guarantees and which one you want and can give. According to this decision implement your internal data handling accordingly to prevent data corruption.

Exception safety is an important concept. It should be a visible part of your object interface. Therefore, you should think about exception safety at the same moment as you define the object interface. Furthermore, document your decision. This will be important for clients and for future maintainers.

Advertisements
Dieser Beitrag wurde unter C++ veröffentlicht. Setze ein Lesezeichen auf den Permalink.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

w

Verbinde mit %s