DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on • Updated on

Implementing “finally” in C++

Introduction

In many functions, you want to guarantee that some “clean-up” function is always called regardless of where or how a function returns. For example, a database class likely will have a close() function that must be called when you’re done with a particular database. In Java, this is accomplished with the finally keyword:

public class DB {
    public DB( String db_name ) {
        // ...
    }

    public void close() {
        // ...
    }
}

public class UseDB {
    public static void main( String args[] ) {
        DB db = new DB( "foo" );
        try {
            // ...
        }
        finally {
            db.close();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The “C++ way” of doing this would be to use a destructor instead:

class DB {
public:
    explicit DB( std::string const &db_name );
    ~DB() { close(); }

    void close();
}

int main() {
    DB db{ "foo" };
    // ...
}
Enter fullscreen mode Exit fullscreen mode

However, suppose you’re using some C library in C++ that has functions like:

DB*  db_open( char const *db_name );
void db_close( DB *db );
Enter fullscreen mode Exit fullscreen mode

In order to guarantee that db_close() is always called, you’d need to do:

int main() {
    DB *const db = db_open( "foo" );
    try {
        // ...
        db_close( db );
    }
    catch ( ... ) {
        db_close( db );
        throw;
    }
}
Enter fullscreen mode Exit fullscreen mode

That is, call db_close() in two separate blocks of code since C++ doesn’t have finally.

One way to solve this would be to write a C++ wrapper class around the C library. If it will be used a lot, this is a good approach. However, if it won’t be used a lot, then writing a full wrapper class might be overkill.

Another way to solve this would be to implement a generic finally in C++ so you could do this:

int main() {
    DB *const db = db_open( "foo" );
    finally f{ [db]{ db_close( db ); } };
    // ...
}
Enter fullscreen mode Exit fullscreen mode

That is, have finally be a class whose destructor will run an arbitrary piece of code. Granted, it doesn’t have the same feel as finally in Java, but it works. It’s actually closer to defer in Go.

Initial Implementation

Here’s a first cut at an implementation of finally in C++:

template<typename CallableType>
class finally {
public:
    explicit finally( CallableType &&callable ) noexcept :
        _callable{ std::move( callable ) }
    {
    }

    ~finally() noexcept {
        _callable();
    }

private:
    CallableType _callable;
};
Enter fullscreen mode Exit fullscreen mode

While this works, it has a number of problems.

Forbidding Copying

The first problem is that copying a finally object will result in the code being run more than once that likely will be the wrong thing to do:

finally f1{ [db]{ db_close( db ); } };
finally f2{ f1 };  // will close the same database twice!
Enter fullscreen mode Exit fullscreen mode

Fortunately, this is easy to fix by deleting the relevant member functions:

    // ...
private:
    CallableType _callable;

    finally( finally const& ) = delete;
    finally( finally&& ) = delete;
    finally& operator=( finally const& ) = delete;
    finally& operator=( finally&& ) = delete;
};
Enter fullscreen mode Exit fullscreen mode

Constraining the Template Type

A less serious problem is what if CallableType actually isn’t callable? What if it’s something like int? The compiler will, of course, print an error message, but it will likely be fairly cryptic and also on a line in the finally implementation itself rather than where the user attempted to declare the finally object.

Fortunately, this is also easy to fix by using the std::invokable concept:

template<std::invocable CallableType>
class finally {
    // ...
Enter fullscreen mode Exit fullscreen mode

Allowing Moving

The initial fix of forbidding copying also forbid moving. If we want to allow moving, that’s possible, but it’s a little more involved in that we need to add an _invoke flag to know whether we should actually invoke the callable since a move’d-from finally must have its _invoke flag set to false:

    // ...
    explicit finally( CallableType &&callable ) noexcept :
        _callable{ std::move( callable ) },
        _invoke{ true }
    {
    }

    finally( finally &&from ) noexcept :
        _callable{ std::move( from._callable ) },
        _invoke{ std::exchange( from._invoke, false ) }
    {
    }

    ~finally() noexcept {
        if ( _invoke )
            _callable();
    }

private:
    CallableType _callable;
    bool _invoke;
    // ...
Enter fullscreen mode Exit fullscreen mode

Allowing move-assignment can similarly be done, but is left as an exercise for the reader.

Forbidding Null Pointers to Function

In addition to lambdas being callable, plain old pointers to function are also callable and should be allowed without needing a lambda:

void clean_up();

int main() {
    finally f{ &clean_up };
    // ...
}
Enter fullscreen mode Exit fullscreen mode

But what if that pointer turns out to be null?

void (*pf)() = nullptr;
finally f2{ std::move( pf ) };
Enter fullscreen mode Exit fullscreen mode

When f2 goes out of scope, its destructor will call a null pointer to function and likely crash. What we need is to check to see whether CallableType is a pointer to function and, if it is, whether it’s null, and set _invoke to false in that case. We can write a helper function:

template<std::invocable CallableType>
class finally {
    template<typename T>
    static constexpr bool if_pointer_not_null( T &&p ) {
        using U = std::remove_reference_t<T>;
        if constexpr ( std::is_pointer_v<U> ) {
            return p != nullptr;
        } else {
            (void)p;
            return true;  // not a pointer: can’t be null
        }
    }
public:
    explicit finally( CallableType &&callable ) noexcept :
        _callable{ std::move( callable ) },
        _invoke{ if_pointer_not_null( _callable ) }
    {
    }

    // ...
Enter fullscreen mode Exit fullscreen mode

Note that we don’t need to check for pointer-to-function specifically since the std::invokable will have already guaranteed that CallableType is invokable; so if it’s a pointer, it must be a pointer-to-function. If CallableType isn’t a pointer, it’s a lambda, and so can’t be null.

Take-Aways

When implementing any general utility class like finally, think about whether all the special member functions make sense:

  • T() — default constructor
  • T(T const&) — copy constructor
  • T(T&&) — move constructor
  • T& operator=(T const&) — copy assignment operator
  • T& operator=(T&&) — move assignment operator

For each one, if it makes sense, implement it; if not, delete it. Also try to think of all possible pathological cases (such as a null pointer to function) and handle them.

Top comments (6)

Collapse
 
serpent7776 profile image
Serpent7776

I don't think I like the idea of adding support for move semantics for such construct. This complicates implementation and increases possibility of an error.
And I'm not sure it adds any value.

Collapse
 
pauljlucas profile image
Paul J. Lucas

Given that move is already implemented, the hard part is already done. As for adding value, I've already used move for finally in a large codebase, so it's already demonstrated its value to me. You don't need it often, but it's just the thing when you do. You're free not to add move in your own implementation.

Collapse
 
serpent7776 profile image
Serpent7776

Could you give an example of situation where moving finally is useful?

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

Unfortunately, I no longer work for the company where I used it, so I don't have access to that source code. I remember it was used is some tricky shutdown code where it needed to be retrofitted. It was only one use out of a codebase that had hundreds of thousands of lines of code, so, like I said, it's not needed often.

That aside, implementing move is like 3 lines of code. I don't consider that "complicates [the] implementation." I'd say the implementation is trivial. Given that, I consider it one of those "why not?" kinds of things. So I don't buy your "complicates" claim.

I also don't buy your "increases possibility of an error" claim without an actual example of where move would create an error, so I turn the burden of proof back on you to provide such an example.

Thread Thread
 
serpent7776 profile image
Serpent7776

I was thinking about the case when you create finally with lambda that captures reference to local object. Then you return that finally object to the caller and bad things happen.
This might be unlikely scenario, but is possible because of move semantics.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

I don't know whether you mean "create a finally within a lambda" like:

auto x = [] {
    finally f{ /*...*/ };
};
Enter fullscreen mode Exit fullscreen mode

or "create a finally with a lambda" like:

finally f{ []{ /*...*/ } };
Enter fullscreen mode Exit fullscreen mode

Regardless, you can capture a reference to a local object in any old lambda just as easily and bad things will happen. You don't need finally to shoot yourself in the foot in that case.