Destructors — 2 use cases

In this post I want to describe an interesting observation: programmers generally use destructors for two purposes. One is fairly obvious: releasing resources; the other — not necessarily so.

One thing that can be objectively said about destructors (i.e., avoiding giving any advice, or judging what is a good/bad practice) is that they are a function that is executed when the object’s life-time ends, and that the call to this function is not visible in the source code.

But it would be unfair to say that a destructor is just an ordinary function. It has one essential special property: throwing exceptions from it is, well… tricky. And it has become even trickier since 2011: unless you explicitly override the default behaviour, an attempt to throw from a destructor results in a call to std::terminate. See this post for details.

This restriction on exceptions does not impede the primary use case for destructors: releasing resources. At this point some readers may object: but releasing a resource may fail too, can’t it? And don’t we signal such failures with exceptions? Let me try to address this concern.

The C++ Standard only defines how exceptions work and interact with other features. The questions like what they are for, where should we use them an where not, are only a matter of opinion or a personal experience. So, I can only offer my personal point of view. Here it is. Exceptions are not meant to simply signal a run-time failure. You use exceptions in the situation where you observe that due to the unavailability of some resources (or for some other, less frequent reasons) performing some actions/computations is impossible or makes no sense, and has to be skipped. Consider:

int fun()
{
  ResourceUser r{params};
  return operate(r);
}

When the acquisition of the resources required by r is impossible you throw an exception. But this is not because you want to broadcast the information that your attempt to acquire the resource failed. You do it because you want to signal that function operate cannot and will not be called; next, to indicate that function fun cannot and will not do what it advertises. Next, consider the outer scope:

int outer_fun()
{
  int i = fun();
  int j = gun(i);
  return hun(j);
}

You throw because in the event of fun not calling operate, you do not want to call gun or hun because they no longer make sense. And consequently, the execution of outer_fun is also ‘cancelled’, as well as the execution of anyone who relies on its results. This avalanche is stopped at some point by an exception handler (a catch-statement): it separates the ‘cancelled’ operations from these that can still be performed.

The point is: you throw not because you want to signal the status of the resources, but because you want to trigger the cancellation of operations. You want some portion of the program to be skipped. The exceptions mechanism gives us a nice tool for controlling how the cancellation avalanche proceeds. Now, let’s go back to our example:

int fun()
{
  ResourceUser r{params};
  return operate(r);
}

Suppose the resource acquisition succeeded and you have successfully evaluated function operate, you have the return value. Before you release the control to the caller, you try to release the resource and you get the feedback that the system didn’t confirm that the resource is released. Sure, it is not nice, but there is no need to cancel anything. Function fun is capable of doing what it advertises: it will return the correct result. Functions gun and hun can be correctly evaluated and in turn, outer_fun can deliver the correct result. You do not need to cancel any operation. True, some other function in the chain (call it foo) can later also attempt to acquire the resource you failed to release, but then it will be reported as foo’s failure to do what it advertises.

For diagnostic purposes, it may be useful to report (somehow) the failure to release a resource, but you need not involve the exception mechanism for this: you do not need to disturb the program logic flow.

You may be convinced or not by the above reasoning, but I think it illustrates well why the restriction on throwing from destructors is not severe, provided you are using destructors for releasing resources.

Destructors that do not release

Another usage of destructors, not uncommon, is to perform a normal action connected to our class’s logic which is naturally the last task to be performed and we do not want to bother users with typing (and possibly forgetting) the obvious. For one example, consider this implementation of DB transaction:

int fun()
{
  DBTransaction trx{parameters};
  trx.execute(sql1);
  trx.execute(sql2);
} // commit or cancel in destructor

For another example, consider a utility class for temporarily storing the value of a given object and later restoring it in the object at the end of the scope:

int doWhatIfAnalysis(Model& m)
{
  StateSaver _{m}; // stores the copy of m's state
  change(m);
  return analyse(m); 
} // restore the saved state of m in destructor               

In each of these cases, in destructor, we perform some program logic advertised by the class, which can fail, and in such cases we want to launch the ‘cancellation cascade’: we need and want to throw exceptions. And here is where the programmer is really hit by the exception mechanism rules. Counter to what some people think, throwing from destructors is allowed and well defined (see this post). However, there is still one issue that remains: the potential double exception issue. I do not want to dive into how the programmers address it. I do not want to advertise this technique too much. I just note that some programmers use destructors this way to the extent that changes to the C++ language are proposed that would allow to check if a destructor is called due to the stack unwinding or not. One of the readers called this technique “logic completion”.

The first use case (resource release) can be safely recommended — it is well thought over in every detail. The second use case is at least controversial — it has some unresolved issues with how to handle destructor failures.

Advertisement
This entry was posted in programming and tagged , , , . Bookmark the permalink.

3 Responses to Destructors — 2 use cases

  1. For a simple example look at HippoMocks’ MockRepository. Its destructor throws if your test otherwise completed fine, but you didn’t match a given expectation. If your test is already failing it’s not too relevant what mock expectations also failed, so it keeps quiet.

  2. bilbothegravatar says:

    Thanks. This topic is thoroughly under-discussed, and blindly following the advice to never throw from a destructor doesn’t really fit IMHO. The design space of “logic completion” technique really needs some more love 😉 (See, e.g. scope guard and friends)

  3. red1939 says:

    I believe the sole reason for disallowing exceptions in dtors is due to the double deletion issue. As we all know there is no great solution for it. In other words what would you do in case when you get an exception while rewinding the stack? Just stop the execution (pretend to raise an exception) and return to the standard exception propagation? I think the committee opted for much more rigid requirements and “cleaner” code. AFAIK in std::thread failure to detach/join results in abort in dtor. Probably they would like to throw an exception ;).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.