Partners: KDAB Whole Tomato Software CppDepend

18 April 2017

final_act - follow-up

final_act follow-up

Last time I wrote about final_act utility, and it seems I’ve opened a bit bigger box that I’ve previously assumed. Let’s continue with the topic and try to understand some of the problems that were mentioned in the comments.

Intro

Let’s remind what was the case last time:

I want to call a custom cleanup code at the end of the scope, and I want to be sure it’s invoked.

bool Scanner::scanNodes()
{
    // code...
    addExtraNodes();
    auto _ = finally([] { removeExtraNodes(); });

    // code...

    return true;
}

I’ve used finally() from GSL that internally works on final_act object.

The most important thing!

OK, I know… I made a typo in the title of my original post! :)
I tried it several times, sent newsletter with the proper name… but the post was wrong :)

GSL -> Guideline Support Library, not GLS -> Guideline Library Support

Important use case

Last time I forgot to mention one huge case where all of those scope_exit/final_act stuff might be utilized.

I mean: transactions. That’s a general term for all of the actions that should be reverted when something fails. If you copied 95% of a file and got an error, you cannot leave such possibly corrupted file; you have to remove it and maybe start again. If you connected to a database and you want to write some records, you assume it’s atomic. I think this idea was ‘hidden’ somewhere in my examples, but it should be more exposed.

So whenever you’re dealing with code that has to be atomic, and transactional, such code constructs might be helpful. Sometimes you can wrap it in a RAII; often explicit code needs to be used.

No exceptions

First of all, my initial assumption was to use final_act in an environment where there are not many exceptions. For example, a lot of legacy code doesn’t use exceptions. Also Google C++ coding guideline doesn’t prefer exceptions (for practical reasons). This is a strong assumption, I know, maybe I did this automatically :)

Without exception handling around, we need to take care only of early returns. In that context, final_act works as expected.

With exceptions

OK… so what are the problems with exceptions then? final_act will work in most cases, so don’t just drop it whenever you have a code with exceptions… but we need to carefully look at some delicate parts here.

First thing: final act is noexcept

As explained many times through the comments in GSL repo (for example here), other issues

And from Final_act can lead to program termination if the final act throws an exception:

Final_act should be noexcept. It is conceptually just a handy way for the user to conjure up a destructor, and destructors should be noexcept. If something it invokes happens to throw, then the program will terminate.

In other words you should write the code that will be called with the same assumptions as other destructor code… so don’t throw anything there. That might be a little limitation when you want to call some ‘normal’ code, not just some clean-up stuff (on the other hand might that would be a bad design after-all?).

I’ve just notices a really great explanation why destructors shoudn’t throws:

from isocpp.org/faq

Write a message to a log-file. Terminate the process. Or call Aunt Tilda. But do not throw an exception!

Throwing from ctor or copy ctor

There’s a long-standing bug in the current implementation:

throwing copy and move constructors cause final_act to not execute the action · Issue #283 · Microsoft/GSL

How to workaround the bug?

We’re looking at this code:

explicit final_act(F f) noexcept 
: f_(std::move(f))
, invoke_(true)
{
}

final_act(final_act&& other) noexcept 
: f_(std::move(other.f_))
, invoke_(other.invoke_)
{
    other.invoke_ = false;
}

And especially those f_(std::move(other.f_)) calls.

The problem will occur if we raise an exception from the move/copy constructor. As I see this, it can happen only with custom move code that we have for the callable object. We should be safe when we use only lambdas as in:

auto _ = finally([] { removeExtraNodes(); });

Since lambdas (update: with no params) will have default code that won’t throw.

So maybe it’s not a major limitation?

update: I missed one thing. Take look at the example provided in the comment at r/cpp. An exception can also be thrown from a copy/move constructor from some argument of the lambda object (since lambdas are 'internally' represented as functor objects and their params are members of that functor). Still, this is probably a quite rare case.

Still, if you plan to use some advanced/custom callable functors, with special move code then it might be good to take something different than final_act.

Other solutions

To be honest, I also assumed that since final_act is proposed in Core Guidelines, then it’s the best choice that we have in Modern C++! But apparently we have some other possibilities:

The talk

First of all please watch this:

CppCon 2015: Andrei Alexandrescu “Declarative Control Flow”

The paper

And read that:

PDF, P0052R3 - Generic Scope Guard and RAII Wrapper for the Standard Library

Roughly, the plan is to have (C++20?) a set of tools:

  • std::scope_exit
  • std::scope_success
  • std::scope_fail

scope_exit is meant to be a general-purpose scope guard that calls its exit function when a scope is exited. The class templates scope_fail and scope_success share the scope_exit’s interface, only the situation when the exit function is called differs. These latter two class templates memorize the value of uncaught_exceptions() on construction and in the case of scope_fail call the exit function on destruction, then uncaught_exceptions() at that time returns a greater value, in the case of scope_success when uncaught_exceptions() on destruction returns the same or a lesser value.

this assumes uncaught_exceptions() returns int not just bool.

folly/ScopeGuard.h

There’s already working code

folly/ScopeGuard.h - master

D Language

In D we have built-in support for such structures:

scope(exit) removeExtraNodes();

see here for some examples Dlang: Exception Safety

Copy elision

The existing code works now and doesn’t rely on Guaranteed Copy Elision that we’ll have in C++17. In order to support this they have to introduce that special bool parameter.

See discussion in Final_act copy/move semantics is wrong

Summary

As it appears final_act is a simple utility that should work well in case where your exit code doesn’t throw exceptions (and also doesn’t throw from copy/move constructors!). Still, if you need some more advanced solutions you might want to wait for general std::scope_exit/_success/_fail utilities.

One of the most important use case is whenever we need transactional approach with some actions. When we require to call some clean-up code after it succeeded or failed.

Meta-blogging-opinion: The beauty of blogging is that often you write about one topic and you unravel (for yourself) a whole new areas. That way blogging is a great way of learning things!

BTW: as a homework you can write a macro FINALLY that wraps the creation of the auto variable and makes sure we have a different name for that variable - so that you might have several final blocks in a function/scope.

Get my free ebook about C++17!

More than 50 pages about the new Language Standard.

C++17 in detail, by Bartlomiej Filipek

For now I don't have my own courses, but I promote others :) (Warning: I'll also get a little commission for every signup). Have a look my recommended C++ courses at @Pluralsight (more info in my Resource page):

© 2017, Bartlomiej Filipek, Blogger platform
Any opinions expressed herein are in no way representative of those of my employers.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.