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();
}
}
}
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" };
// ...
}
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 );
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;
}
}
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 ); } };
// ...
}
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;
};
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!
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;
};
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 {
// ...
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;
// ...
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 };
// ...
}
But what if that pointer turns out to be null?
void (*pf)() = nullptr;
finally f2{ std::move( pf ) };
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 ) }
{
}
// ...
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)
Then you can do things like:
defer { [db]{ db_close( db ); } };
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.