Friday, November 29, 2013

D's scope statement in C++

This article is at Version 2 as of:
November 30th 2013
Online discussion of this article may be outdated.


The D Programming Language brings a unique feature: The scope statement. In this C++ secret, I will explain how to implement it in C++.

Disclaimer


Some parts here get tricky, and I don't claim to be a C++ god. If something looks strange to you, then something could really be wrong. Do some research and please tell me what you think!

Introduction


For The Innocents among you who don't know about D's scope feature, please read Dlang's official documentation for a good explanation.

In summary, the scope statement allows you to specify blocks of code to be run at the end of their scope, and conditionally executed based on whether the scope was exited by an exception or not.

In D, the usage is as follows (from dlang.org):
void abc()
{
    Mutex m = new Mutex;

    lock(m); // lock the mutex
    scope(exit) unlock(m); // unlock on leaving the scope

    foo(); // do processing
}
In normal everyday C++, we use RAII to achieve the same effect:
struct lock_guard {
    std::mutex& m_;
    lock_guard(std::mutex& m) : m_(m) { m_.lock(); }
    ~lock_guard() { m_.unlock(); }
};

void abc()
{
    std::mutex m;
    lock_guard guard { m }; // Locks the mutex.
    foo();    
    // ~lock_guard() unlocks the mutex upon leaving the scope.
}
The beauty of this pattern is that ~lock_guard() is guaranteed* to be called upon unwinding the stack, no matter if foo() throws an exception or not. This is an important part of writing exception-safe code.

[*] Nothing is guaranteed in C++: Destructors are not invoked if a thrown exception will never be caught, because in this scenario no exception handler is installed. Also, if a destructor leaks out an exception while another exception is in flight, then all bets are off.

The custom lock_guard class is not necessary, it is part of the C++ standard library.

This might look all good so far, but the problem arises when, for example, we want to use a C library which does not offer convenient C++ RAII resource handles like std::lock_guard. This forces exception-conscious C++ users to write tons of little RAII resource handling classes. On the other hand, if C++ had a scope statement, these boilerplate RAII bindings would not be necessary!

The Goal


Enough talk, here is my promise to you. This C++ Secret allows the following code:
void abc()
{
    // We are manipulating a non-RAII C resource.
    pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&m); // lock mutex
    scope (exit) {
        // unlock on leaving the scope
        pthread_mutex_unlock(&m);
    };
    foo(); // Do dangerous work which may throw an exception
}
Come hell or high water, this code will not leak the mutex. It will always be unlocked upon exiting the scope of that function, whether through normal code flow or through an exception being raised in foo().

Scope Guard


Everything begins with the scope_guard class, which will store a function object to call upon cleanup, a policy for how that cleanup should be used, and a policy for installing the scope block.
template<typename F, typename CleanupPolicy>
class scope_guard : CleanupPolicy, CleanupPolicy::installer
{
    using CleanupPolicy::cleanup;
    using CleanupPolicy::installer::install;
 
    typename std::remove_reference<F>::type f_;
 
public:
    scope_guard(F&& f) : f_(std::forward<F>(f)) {
        install();
    }
    ~scope_guard() {
        cleanup(f_);
    }
};
This class will store a routine to run at the end of the scope, and the CleanupPolicy has the right to decide how to invoke it. More on the CleanupPolicy's installer later.

Note the use of perfect forwarding to handle both lvalue and rvalue references in one fell swoop.

Installation Policies


These polices define some code to be run at the initialization of a scope_guard.
struct unchecked_install_policy {
    void install() { }
};
 
struct checked_install_policy {
    void install() {
        if (std::uncaught_exception()) {
            std::terminate(); // sorry
        }
    }
};
The installation policies solve an important problem: If an exception is currently in flight, a scope (success) or scope (failure) statement is not able to distinguish between that current exception and any other one thrown after it in the same scope. In this case, the scope (success) and scope (failure) statements behave incorrectly because they can confuse one exception for another.

The only time when an exception is "currently in flight" is during the call to destructors while unwinding the stack. Therefore, we establish and enforce an important invariant to be upheld by the programmer:

ONE CANNOT USE EITHER OF
scope (failure)
scope (success)
WITHIN A DESTRUCTOR!

This includes functions called from destructors.

scope (exit), on the other hand, is always ok. Knock yourself out!

Cleanup Policies


Next, we will define the cleanup policies. Their behaviours are:
  • exit: cleanup will happen come hell or high water.
  • failure: cleanup will happen only if the scope is exited by an exception.
  • success: cleanup will happen only if the scope is exited normally.
An appropriate installation policy is also defined for each cleanup policy.
struct exit_policy {
    typedef unchecked_install_policy installer;

    template<typename F>
    void cleanup(F& f) {
        f();
    }
};

struct failure_policy {
    typedef checked_install_policy installer;

    template<typename F>
    void cleanup(F& f) {
        // Only cleanup if we're exiting from an exception.
        if (std::uncaught_exception()) {
            f();
        }
    }
};

struct success_policy {
    typedef checked_install_policy installer;

    template<typename F>
    void cleanup(F& f) {
        // Only cleanup if we're NOT exiting from an exception.
        if (!std::uncaught_exception()) {
           f();
        }
    }
};
As you can see, exit_policy calls the cleanup unconditionally. Meanwhile, the failure_policy and success_policy use std::uncaught_exception() to check if an exception is currently "in flight" as we are unwinding the stack, which allows them to decide if the cleanup should be made or not.

Furthermore, exit_policy has no checking required during the installation, while failure_policy and success_policy enforce the invariant that no exceptions can be in flight at the time of installing the guard. WITH DEADLY FORCE!

We can test what we have so far:
void SayBye()
{
    std::cout << "Bye bye!\n";
}

int main()
{
    scope_guard<void (*)(), exit_policy>
        sayByeOnExit { SayBye };

    std::cout << "Hello!\n";
}
Output:
Hello!
Bye bye!
As expected, the call to SayBye is deferred until we exit from the scope of main().

Syntactical Sugar


Now that we have nailed down the mechanics, here comes the syntactical sugar.
template<typename CleanupPolicy>
struct scope_guard_builder { };
 
template<typename F, typename CleanupPolicy>
scope_guard<F,CleanupPolicy>
operator+(
    scope_guard_builder<CleanupPolicy> builder,
    F&& f
    )
{
    return std::forward<F>(f);
}

// typical preprocessor utility stuff.
#define PASTE_TOKENS2(a,b) a ## b
#define PASTE_TOKENS(a,b) PASTE_TOKENS2(a,b)

#define scope(condition) \
    auto PASTE_TOKENS(_scopeGuard, __LINE__) = \
        scope_guard_builder<condition##_policy>() + [&]

int main()
{
    scope (exit) {
        std::cout << "Bye bye!\n";
    };

    std::cout << "Hello!\n";
}
Output:
Hello!
Bye bye!
An instance of scope_guard is put on the stack with an automatically generated name made from "_scopeGuard" and the current line number.

Since C++ cannot infer template arguments from arguments passed to a constructor, we cannot simply assign a lambda to a scope_guard. Instead, we use a factory function, which happens to be operator+.

The first argument is an empty struct scope_guard_builder, which is only used to infer the CleanupPolicy type. The condition argument to the scope macro is used to deduce the cleanup policy to use.

The second argument is a function object, which is used to construct the returned scope_guard. The scope macro passes a lambda, which captures its enclosing scope's automatic variables by reference, as this second argument.

operator+ was chosen simply because it is an infix operator. If a regular function would have been used instead of an infix operator, the usage would look something like:
scope (exit) {
    std::cout << "bye\n";
});
Note the outstanding right parenthesis at the end, which would be required to end the regular (prefix notation) function call to which the lambda would be passed.

Instead, the lambda's capture list is part of the macro ("[&]") and the lambda's body is conveniently used as the syntax for the scope statement's block. This also allows you to enhance scope blocks by configuring their lambda's mutability, exception specification, and attribute specification.

Conclusion


To conclude, here is a relatively robust unit test.

The runtime error at the end is intentional - it shows that we enforce the rule that only scope (exit) can be used in destructors.
struct HasScopeExitInDtor {
    ~HasScopeExitInDtor() {
        scope (exit) {
            std::cout << "scope (exit) in dtor success test\n";
        };
        try {
            std::cout << "scope (exit) in dtor failure test\n";
            throw 1;
        } catch (...) { }
    }
};

struct HasScopeSuccessInDtor {
    ~HasScopeSuccessInDtor() {
        std::cout << std::flush;
        scope (success) {
            std::cout << "error: scope (success) used in dtor\n";
        };
    }
};

int main()
{
    {
        const char* captureTest = nullptr;
        scope (exit) {
            std::cout << "scope (exit) success test\n";
            std::cout << "captureTest: " << captureTest;
        };
        scope (success) {
            std::cout << "scope (success) success test\n";
        };
        scope (failure) {
            std::cout << "scope (failure) success test\n";
        };
        captureTest = "ok\n";
    }
    try {
        HasScopeSuccessInDtor s;
  
        scope (exit) {
            std::cout << "scope (exit) failure test\n";
        };
        scope (success) {
            std::cout << "scope (success) failure test\n";
        };
        scope (failure) {
            std::cout << "scope (failure) failure test\n";
        };
  
        HasScopeExitInDtor e;
  
        throw 1;
    } catch (...) { }
}
Output:
scope (success) success test
scope (exit) success test
scope (exit) in dtor failure test
scope (exit) in dtor success test
scope (failure) failure test
scope (exit) failure test
terminate called without an active exception
Beautiful.

Here is the fully functional example: http://ideone.com/2IpHBG

In general, I believe that this makes it easy to write code that is readable and exception-safe. There is only a minor runtime performance cost.

I would personally like scope to be a built-in feature of C++ so that it could be better supported by the compilers themselves.

One downside to this implementation is that the unique name generated by pasting _scopeGuard with __LINE__ disallows using more than one scope statement on the same line. One option is to use __COUNTER__ instead, but it's not standard. This is a minor use case, so I'll let it slide for now.

The major downside is the inability to call scope (success) or scope (failure) within a destructor. Some alternate implementations below address this in interesting ways.

Special Thanks


Thanks to edoceo` in ##C++ on irc.freenode.net for various suggestions and valuable criticism.

Thanks to The One And Only Andrei Alexandrescu for explaining to me the bug of calling scope (success) and scope (failure) within destructors.

Thanks to my colleague, JP Flouret, for helping me remember that, sometimes, less is more.

Further Research


Here are some alternate implementations of the ScopeGuard pattern:

Github


https://github.com/nguillemot/scope
  

15 comments:

  1. Your solution relies on using std::uncaught_exception, which is not safe for this (or any other) purpose. Herb Sutter has a good write-up on this topic: http://www.gotw.ca/gotw/047.htm

    His final conclusion: "Unfortunately, I do not know of any good and safe use for std::uncaught_exception. My advice: Don't use it."

    ReplyDelete
    Replies
    1. Thanks for sharing. This is very good information to be aware of!

      Delete
    2. Maybe I'm not reading into it deep enough, but this seems more like a philosophical argument than a technical one?

      Delete
    3. No, it's not just a philosophical argument. Put in the simplest terms, uncaught_exception is broken, since it does not take into account local conditions. If, in a destructor, I put a function call that performs work within a try block, and uses your success/failure policies, then if that destructor is called during stack unwind due to an exception, then even if the nested function performed correctly, it will still choose the failure path.
      For uncaught_exception to work correctly it would have to take into account whether it is called from try blocks started during stack unwinding, dive in that case there isn't an uncaught reception in the local scope.
      If you'd like me to code up a small example that shows how this would cause problems with scope, let me know.

      Delete
    4. Apologies for some misspellings BTW, typing on a tablet...

      Delete
    5. You are totally right, I have been shown the light. I have rewritten my article to address this issue with version 2. I would be glad if you could review it and tell me what you think!

      Delete
    6. I've reviewed your changes, it looks good. Thanks for the article btw, in retrospect I must say I found it thought-provoking in terms of dealing with RAII and cleanup issues.

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Basically what Jan-Jaap said, proceed with caution. Here's a proposal from Herb for a `std::unwinding_exception` which didn't make it into C++14 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3614.pdf

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Hi Nicolas,

    Thank you for a really interesting and cognitive article! It shows up a number of nifty tricks and demonstrates a fusion of several solid approaches.

    Apart from the comment above regarding the use of std::uncaught_exception (which I believe is a point worth deeper inspection) I'd suggest wrapping the scope_guard destructor's body with try/catch(...) as in case if supplied functor throws an exception it'll yield a call to std::terminate.
    The reference article is the one from Andrei Alexandrescu at Dr.Dobbs where he also recommends this additional try/catch protection in the destructor for his ScopeGuard implementation proposal.

    ReplyDelete
    Replies
    1. Not a bad idea, but I think that disallowing throwing exceptions out of destructors is an invariant not worth overriding. In the case where the body of the lambda is entirely noexcept, the effort is wasted.

      Delete
  6. Nice article, but the locking does not work if you use a locally defined mutex, i.e., like so:

    std::mutex m; // locally defined mutex
    lock_guard guard { m }; // Locks the mutex.

    In this case each thread which executes the critical section has its *own* mutex and there is no exclusion. Multiple threads can happily execute your 'guarded' code at the same time.

    Instead, you can make the mutex a global variable (a private member of your class, typically mutable, if you have classes) or with a compiler that implements the C++11 memory model (gcc 4.8, ...) you can also use a static variable. For older compilers, however, you don't get a guarantee that static variables get initialised only once.

    ReplyDelete
    Replies
    1. You're totally right, it is a contrived example. Oh well!

      Delete