Implementing “finally” in C++

Paul J. Lucas - May 24 '23 - - Dev Community

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 forbids 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.

defer

If you’d prefer either to be more Go-like or not to have to make up names for finally variables, you can use a few macros:

#define NAME2(A,B)           NAME2_HELPER(A,B)
#define NAME2_HELPER(A,B)    A ## B
#define UNIQUE_NAME(PREFIX)  NAME2(NAME2(PREFIX,_),__LINE__)

#define defer                finally UNIQUE_NAME(defer)
Enter fullscreen mode Exit fullscreen mode

Then you can do things like:

defer { [db]{ db_close( db ); } };
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .