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.
Top comments (6)
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.
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.Could you give an example of situation where moving finally is useful?
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.
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.
I don't know whether you mean "create a
finally
within a lambda" like:or "create a
finally
with a lambda" like: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.