Correctly implement finally block using C++ lambda

2019-03-08 11:24发布

问题:

I want to implement a finally block in my C++ program, and the language certainly has the tools to do it if not a native facility. I was wondering what the best way to do this is?

回答1:

This simple implementation seems to be 100% safe.

template< typename t >
class sentry {
    t o;
public:
    sentry( t in_o ) : o( std::move( in_o ) ) {}

    sentry( sentry && ) = delete;
    sentry( sentry const & ) = delete;

    ~ sentry() noexcept {
        static_assert( noexcept( o() ),
            "Please check that the finally block cannot throw, "
            "and mark the lambda as noexcept." );
        o();
    }
};

template< typename t >
sentry< t > finally( t o ) { return { std::move( o ) }; }

The noexcept is important because you don't want to throw an exception when your function is already exiting due to an exception. (That leads to immediate termination.) C++ doesn't check that the lambda really can't throw anything; you manually check and mark it noexcept. (See below.)

The factory function is necessary because otherwise there's no way to obtain a type dependent on a lambda.

The copy and move constructors must be deleted because they could be used to implicitly generate a temporary object, which would implement another sentry, which would call the block prematurely upon destruction. But the default assignment operator is left intact because if you already have two sentries that do different things, it's OK to assign them. (Somewhat theoretical, but whatever.)

It would be nice if the constructor were explicit, but that seems to preclude in-place initialization of the return value. Since the class is not movable, the object living in the caller's scope must be initialized directly by the expression in the return statement.

To use, just define a guard like this:

auto && working_state_guard = finally( [&]() noexcept {
    reset_working_state();
} );

It's essential to bind to a reference, because declaring a real object in the calling scope would require move-initializing that object from the function return value.

Circa version 4.7, g++ -Wall would give a warning that the guard is unused. Whether or not you're coding against that, you can add a little safety and documentation at the end of the function with an idiom:

static_cast< void >( working_state_guard );

This lets the reader know about execution of code from the beginning of the scope, and it can serve as a reminder to double-check when copy-pasting code.


Usage.

int main() {
    auto && guard = finally( []() noexcept {
        try {
            std::cout << "Goodbye!\n";
        } catch ( ... ) {
            // Throwing an exception from here would be worse than *anything*.
        }
    } );

    std::cin.exceptions( std::ios::failbit );
    try {
        float age;
        std::cout << "How old are you?\n";
        std::cin >> age;
        std::cout << "You are " << age << " years (or whatever) old\n";
    } catch ( std::ios::failure & ) {
        std::cout << "Sorry, didn't understand that.\n";
        throw;
    }
    static_cast< void >( guard );
}

This produces output like

$ ./sentry 
How old are you?
3
You are 3 years (or whatever) old.
Goodbye!
$ ./sentry 
How old are you?
four
Sorry, didn't understand that.
Goodbye!
terminate called after throwing an instance of 'std::ios_base::failure'
  what():  basic_ios::clear
Abort trap: 6

How to cancel the action from being performed?

Looking at some of the "previous attempts," I see a transactional commit() method. I don't think that belongs in the ScopeGuard/finally block implementation. Implementing a protocol is the responsibility of the contained functor, so the correct division of labor would be to encapsulate a Boolean flag in it, such as by capturing a bool local flag, and flipping the flag when the transaction is complete.

Likewise trying to cancel the action by reassigning the functor itself is just a messy approach. Generally prefer an extra case inside the existing protocol, to inventing a new protocol around the old one.



回答2:

An alternative solution using std::function. No factory function needed. No per-use template instantiation (better footprint?!). No std::move and && stuff needed, no auto needed ;)

class finally
{
    std::function<void()> m_finalizer;
    finally() = delete;

public:
    finally( const finally& other ) = delete;
    finally( std::function<void()> finalizer )
     : m_finalizer(finalizer)
    {
    }
    ~finally()
    {
        std::cout << "invoking finalizer code" << std::endl;
    if( m_finalizer )
        m_finalizer();
    }
};

Usage:

int main( int argc, char * argv[] )
{
    bool something = false;
    try
    {
    try
    {
            std::cout << "starting" << std::endl;
        finally final([&something]() { something = true; });
        std::cout << "throwing" << std::endl;
        throw std::runtime_error("boom");
    }
    catch(std::exception & ex )
    {
        std::cout << "inner catch" << std::endl;
        throw;
    }
    }
    catch( std::exception & ex )
    {
    std::cout << "outer catch" << std::endl;
        if( something )
    {
        std::cout << "works!" << std::endl;
        }
        else
    {
        std::cout << "NOT working!" << std::endl;
        }
    }
    std::cout << "exiting" << std::endl;
    return 0;
}

Output:

starting

throwing

invoking finalizer code

inner catch

outer catch

works!

exiting