The obvious final step

The title may be misleading, as I had to invent a new short term for the pattern that occurs in the code once in a while. Example first:

// write an element of an XML file

xml.begin_element("port");
xml.attribute("name", name);
xml.attribute("location", loc);
xml.end_element("port");  // <-- the obvious final step

During the construction of an XML file when you write an element, it is obvious that the last thing that you do is to write the closing tag. By obvious we mean:

  • Writing it down adds no new information (there is no other possible final instruction for this task).
  • It would be a bug if this instruction wasn’t there.

For this reason, people invent tools that allow expressing this type of business logic in a way that does not require of the programmer to write down the obvious final step. An oft used technique is to employ destructors. However, this is very difficult to get right and not implementable in general. In this post we will see why, and we will also see an alternative.

XML elements is just one example. For another one, consider a database transaction where the obvious final step is to commit it:

DBTransaction trx{parameters};
trx.execute(sql1);
trx.execute(sql2);
trx.commit();  // <-- the obvious final step

Writing down the obvious final step is not a bad idea. This way the code clearly reflects what instructions will be executed in the program. This makes it easier to reason about the program when we need to go low-level, for instance when debugging.

The problem with this is that it is easy for the programmer to forget to write the obvious final step. It wouldn’t be a problem in a different, imaginary language or IDE where the machine would recognize a library with an obvious final step, and signal a compiler/IDE error when the programmer forgets it, or if it put the obvious final step into the code for the programmer. But this is fantasy. In C++, the best we can do is to provide a construct that does the obvious final step implicitly. There is a number of ways to do it.

Abusing destructors

One way to achieve our goal is to introduce a type whose destructor would perform the obvious final step. This applied to the XML example could look something like this:

{
  ElementScope port = xml.element_scope("port");
  xml.attribute("name", name);
  xml.attribute("location", loc);
} // <-- the obvious final step in the destructor

Indeed, the closing element tag is now printed without programmer having to write anything at the end. But this only works to a certain point, and can very easily go wrong when anything here reports failure via an exception. Destructors were not designed to handle that case.

First, the obvious final step is a piece of business logic that can fail, and throw. As explained in this post, since C++11 destructors are implicitly declared noexcept. Any attempt to signal failure (via an exception) will result in a call to std::terminate(). Because of this change, libraries that used to employ destructors for the obvious final step suddenly broke in C++11. In order to fix this problem, you have to explicitly annotate your destructor with noexcept(false):

// write an element of an XML file

ElementScope::~ElementScope() noexcept(false)
{
  _xml.end_element(_name);
}

But it only addresses one problem. There are more. Destructors were designed to release resources. You release a resource regardless of whether the previous operations that used the resource failed or not. This is unlike the normal business logic, which you wish to cancel when the previous operation failed. This has been explained in this post. Going back to our example, if the serialization of one of our attributes fails,

{
  ElementScope port = xml.element_scope("port");
  xml.attribute("name", name);
  xml.attribute("location", loc); // <-- if this fails...
} 

Then the destructor will be called, and will try to close the element, even though we failed to serialize all attributes. Maybe we will get away with this, but in general executing a dependent operation when the previous one failed is a logic error. Worse still, serializing the closing tag can also fail and throw a second exception, which will end in calling std::terminate().

In order to address this, we would have to know if the destructor of port was called due to an exception thrown during the life time of port. This is not doable in general. But we might still want to give it a shot. Until C++20 we had function std::uncaught_exception() which says if in the current thread there is at least one uncaught exception: one that was thrown (or re-thrown) and not yet arrived at a handler (the matching catch-statement or std::terminate()). This is almost what we need but not quite. This is because you might have a stack full of uncaught exceptions in your thread and still call destructors not becuse of an exception. This is explained in this post. This is because when we call functions during stack unwinding, primarily in destructors, inside such destructor we perform normal function calls, and inside each such call you can do almost anything, including catching and throwing exceptions. Here is an example.

// C++11

#include <stdexcept>
#include <cassert>
 
struct InnerType 
{
  ~InnerType()  {
    assert(std::uncaught_exception());  /*4*/
  }
};

void innerWorld()
{
  assert(std::uncaught_exception());    /*3*/
  InnerType innerObj;
} // innerObj destroyed *not* due to exception


struct OuterType
{
  ~OuterType() try {
     innerWorld();                      /*2*/
  }
  catch(...) {}
};
 
int main() 
try {
  OuterType outerObj;
  throw std::runtime_error("");         /*1*/
} // innerWorld() starts in outerObj's destructor
catch(std::runtime_error const&) {
  assert(!std::uncaught_exception());
}

In this example, function innerWorld() is called indirectly due to stack unwinding. However, inside innerWorld() everything is an isolated environment, you can call functions, create and destroy objects, but if you inspect the progrm state with std::uncaught_exception() it will return true, because it can see the uncaught exception in the outer world.

Maybe this is not a problem in practice. Maybe you will not be serializing XML or starting DB transactions inside destructors of other objects. Maybe you can make a good life with that solution. But your function/library can be called inside the inner world of somebody else who decides to launch an inner world inside a destructor, and you may not even know about it. The solution with std::uncaught_exception() is in general incorrect, because std::uncaught_exception() does not give you an answer to the quesiotn you really ask: was the destructor of my object called due to an exception thrown after the life time of object started.

This is why std::uncaught_exception() has been removed from C++20 and instead, since C++17, we have std::uncaught_exceptions() (plural form) which gives you the number of uncaught exceptions in a given thread. Using it you can measure the number of uncaught exceptions in the constructor, then measure the same number in the destructor, and if the latter is greater (hopefully by one) you have your answer: an exception has been thrown after the life time of object started:

// write an element of an XML file
// C++17

ElementScope::ElementScope(std::string_view name)
  : _name{name}
  , _countOnEntry{std::uncaught_exceptions()}
{
  _xml.begin_element(_name);
}

ElementScope::~ElementScope() noexcept(false)
{
  int countOnExit = std::uncaught_exceptions();
  if (countOnExit == _countOnEntry)
    _xml.end_element(_name);
}

And this solution works in C++17. But it no longer works in C++20, because now we have coroutines. A coroutine can be suspended at an arbitrary point, with some state of the thread’s uncaught exceptions stack, and later resumed on a different thread, with the other thread’s uncaught exception stack, or even on the same thread but way later, when the uncaught exceptions stack has significantly changed. Here is an example using a cppcoro::generator from Lewis Baker’s CppCoro library. A generator is a tool that exposes a range-based interface for a coroutine yielding a sequence values:

// C++ 20

cppcoro::generator<int> zeroOne()
{
  ElementScope port{"port"};
  co_yield 0;
  throw std::runtime_error("");
  co_yield 1;
}

The call to zeroOne() will return an object of type cppcoro::generator<int> which expeoses a range interface. This range was intended to produce two values: first 0, then 1. We are emulating the situation where the attempt to produce the second value fails and this is reported via an exception. The second value will not be yielded, but the destructor of port will still be called, and we expect it to do nothing. Our usage of cppcoro::generator<int> is a bit contrived; the goal is to illustrate the point:

void start();  // get 1st value from coroutine
void finish(); // get 2nd value from coroutine

struct StartInDestructor
{
  ~StartInDestructor() {
    assert(std::uncaught_exceptions() == 1);
    start();
  }
};

int main()
try {
  try {
    StartInDestructor s;
    throw 1;
  }
  catch (int) {}

  assert(std::uncaught_exceptions() == 0);
  finish(); 
}
catch (...) {}

The idea here is that the first value from the coroutine will be obtained from function start(), at the point where we have one uncaught exception. This is what the constructor of port will record. The user of a coroutine has one uncaught exception, inside the coroutine there is no exception, but std::uncaught_exceptions() cannot detect this subtle difference. Later, we call finish() which tries to consume the second value from the coroutine. At this point the caller has zero uncaught exceptions, but when the coroutine is resumed it throws one, so the destructor of port, called due to an exception, will record value one. There is no exception in the caller but there is one in the coroutine: the opposite of what we have seen before. As a result, value one has been recorded both in the constructor and in the destructor, so the obvious final step in the destructor is executed, even though it has been called directly because of an exception in the coroutine.

For completeness, the following are the definitions of start() and finish():

std::optional<cppcoro::generator<int>> gen;
cppcoro::generator<int>::iterator it;

void start()
{
  assert(std::uncaught_exceptions() == 1);

  gen = zeroOne();    // obtain generator
  it = gen->begin();
  *it;                // get the first value
}

void finish()
{
  assert(std::uncaught_exceptions() == 0);

  ++it;  // resume the coroutine, this will cause
         // a throw in the coroutine

  *it;   // these will be skipped
  ++it;  //
}

The full example can be found here; a playable example in Compiler Explorer is here.

To put it in other words, std::uncaught_exceptions() cannot help detect a new exception in scope in situations where we have multiple tasks sharing the same thread’s state.

Long story short, there is no known way to implement the obvious final step in the destructor that would work in all cases. You might say that the situations in which the problem will manifest are very rare. That is correct, but does this speak in favour or against employing this technique? In my experience, bugs that occur very seldom are the worst kind, as they find you completely unprepared.

Not to mention the runtime cost: the call to std::uncaught_exceptions() is an opaque call to the runtime that is not optimized out, even in -O3.

Inversion of control

Given the above, the option to type the obvious final step in your code doesn’t seem that bad. But there is a yet another way. Your library can provide a wrapper function that performs the initial work, then calls the user code in the form of a callback, and then performs the obvious final step:

// Your code:

template <typename Callback>
void XML::with_element(std::string_view name, Callback userLogic)
{
  begin_element(name);
  userLogic();
  end_element(name);
}

// User code:

xml.with_element("port", [&] {
  xml.attribute("name", name);
  xml.attribute("location", loc);
});

Here, the obvious final step is executed in a well defined place, indicated by }), but this is normal code — no destructors — subject to normal rules of failure handling and canceling operations, as explained in this post. The scope of the user code, between the begin and end of our element, is clearly embraced visually. A lambda with the default reference capture makes sense, as we know it will not leave our scope, and is an optimization opportunity.

Any use of a lambda, as well as any inverison of control, makes code more difficult to read or debug, but if one insists on the implicit obvious final step, this may be a more reliable technique than destructors.

The DB transaction example rewritten in this way would read:

db_transaction(parameters, [&]{
  trx.execute(sql1);
  trx.execute(sql2);
};

Further reading

In the examples above I used the implementation of generator from Lewis Baker’s CppCoro library. Lewis also has a series of intresting posts on coroutines at Asymmetric Transfer.

Posts related to exception handling and destructors:

Handling errors is canceling operations

noexcept destructors

Destructors that throw

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

12 Responses to The obvious final step

  1. Anto says:

    Better solution to me is
    “`
    class XMLWriter {
    bool should_write_end = false;
    bool should_write_start = true;
    bool finish_write = true;
    public:
    ~XMLWriter() {
    if (finish_write && should_write_end) {
    should_write_end = false;
    write_end();
    }
    }
    void write_element(const std::string& name, const std::string& value) {
    finish_write = false;
    should_write_end = true;
    if (should_write_start) { write_start(); should_write_start = false; }
    /* … */
    finish_write = true;
    should_write_end = true;
    }
    };
    “`

  2. Tomaz Canabrava says:

    Just… compile the code with no exceptions and everything is fixed, raii can be applied and the universe is happy.

  3. Another example why exceptions are evil and shouldn’t be used (unless they’re REALLY beneficial, which they usually are not, but sometimes can be).

    • Helmut Zeisel says:

      What would be the difference if you use e.g. std::expected instead of an exception?

      • Jordan Kaye says:

        std::expected can’t randomly pop up and unwind your stack because another thread/coroutine did something naughty. It makes the control flow deterministic, which removes all of the problems that exceptions cause in this post

        • The way I see it, the problem in this post is not caused by exceptions, but by the abusage of destructors. I described the “idea” that I observed a number of times people come up with (to use destructors to implement some business logic), which seems fine on the surface, but cannot really work, becuse it does not take into account how destructors work and what they are for. (I tried to explain it in this post.)

          Exceptions do not pop randomly. They pop up whenever a failure (of a function to do what it advertizes) needs to be reported. In that situation what you need to do in 95% of the cases is to propagate the information about this failure up the stack, making sure that resoures are cleaned up, and any other operations skipped. This is what exceptions do. If you do not use them, you have to do it manually. And doing manually a lot of repeatitive code is bug-prone. The only case where manual error propagation is better is when most of your functions never fail, or when they fail terminally and the propagation doesn’t matter anyway.

          From the mechanics of std::uncaught_exceptions someone could draw a conclusion that exceptions are evil: “I tried to use std::uncaught_exceptions to do clever stuff in destructors, it didn’t work, so exceptions are evil.” But I would not agree with such conclusion. The way I see it, std::uncaught_exceptions is useless, because it is like a global: different agents can use it for different unrelated things at different times. for the same reasons we are advised not to rely on globals where this is not necessary.

  4. Anonymous says:

    Just make sure you don’t need to do an early return/break/continue from within such a callback.

  5. Abel says:

    Thank you, it is a great post!

    Let me share an other approach, which can enforce a required – but accidentally omitable – step. The idea is from Curry-Howard correspondence, which connects programming and mathematics (logic) by saying that
    – types can be considered as propositions
    – struct as the logical AND operator (one can instantiate only if every member can be instantiated
    – dispatched union (optional, variant) are the logical OR operation
    – function call is the implication (if argument types can be instantiated then the return type can be instantiated too. etc.

    By using this, I can write the commitTransaction() function to return a ClosedTransaction object, which otherwise could not be instantiated. And the enclosing function return type is ClosedTransaction, so commit must be called.

    It might require a bit of thinking and work, but I prefer commitTransaction to be an explicit step in my code. And now it is so explicit that the compiler verifies for us.

    Ps. This work really well for starting or intermediate step and maybe a bit harder to apply for the final step where the result is discarded anyway.

  6. Helmut Zeisel says:

    What about e.g. std::fstream and std::scoped_lock? They are also doing “obvious” work in the destructor (free, unlock). Are these std classes good good or bad?

    • Thanks for this question, especially about the `std::fstream`.

      The answer for the lock types is easy. All the use cases that I know of is that they represent temporarily owning a resource (a piece of data protected by mutex). Managing this resource is not part of the function’s postcondition, so (1) you should not throw from it and (2) you should release it even if you throw an exception to signal the failure to deliver the postcondition.

      The situation with `std::fstream` is trickier, in part because IOStreams were designed before C++ had exceptions. (See https://stackoverflow.com/questions/3180268/why-are-c-stl-iostreams-not-exception-friendly)

      The exception support was retrofitted, with limited success. For instance, you have to opt in to throwing exceptions, but not in constructors.

      So, what you typically do with an IOStream is buffering: you write things to a buffer. At some point you have to flush this data from the buffer into a more appropriate place, but not necessarily the ultimate destination, such as a file, but maybe some file buffer in the OS. So this flushing does not necessarily guarantee a full success, but you still have to answer the question if this flushing is the “obvious final step”.

      The answer here is more dependent on the use case. Often IOStreams are used for logging. And logging is one of these cases, where you do not want to stop the program or other operations or even signal when you fail.

      However, when your function’s contract is that you commit to storing some data into file (or at least flushing it to the external buffer representing a file), then my recommendation is this flush should not happen in destructor: either type it explicitly, or use the callback trick for calling it. You have functions `flush()` and `close()` for this purpose.

  7. Pingback: 当たり前の最終手段 – 世界の話題を日本語でザックリ素早く確認!

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.