There are a bunch of cases where there is cleanup that needs to happen before a function finishes, regardless of the exit path. I’ve talked about how you can accomplish this in C with GNU extensions in the past. Today I want to talk about doing the same thing in C++ (hint: it’s a lot more flexible).

C++ heavily leans into the concept of RAII (resource acquisition is initialization). This concept ensures that resources are initialized properly once an object has been constructed and everything gets cleaned up when that object is no longer needed (i.e., exits scope). You see this in a bunch of standard library classes like std::vector, std::unique_ptr, and std::shared_ptr. We can use these concepts to write a class that on construction takes a function that will be run whenever an instantiation of that class exits scope.

template <typename action> class defer
{
  static_assert ( std::is_invocable_v<action>,
                  "defer requires a callable with no parameters" );
  static_assert (
      std::is_void_v<std::invoke_result_t<action>>
          || std::is_same_v<std::invoke_result_t<action>, void>,
      "defer callable should return void (return value will be ignored)" );

  action m_action;
public:
  defer ( const defer & ) = delete;
  defer ( defer && ) = delete;
  auto operator= ( const defer & ) -> defer & = delete;
  auto operator= ( defer && ) -> defer & = delete;
  explicit defer ( action &&act ) : m_action ( std::move ( act ) ) {}
  ~defer () { m_action (); }
};

The defer class can then be used to wrap a lot of different C-style libraries that have cleanup functions that must be called to prevent resource leaks. Here’s an example using it with the sqlite3 library.

void
create_table ( const char *db_path )
{
  sqlite3 *db = nullptr;

  if ( sqlite3_open ( db_path, &db ) != SQLITE_OK )
    {
      std::cerr << "Can't open database\n";
      return;
    }

  defer closeDb ( [db] () -> void { sqlite3_close ( db ); } );

  const char *sql = "CREATE TABLE IF NOT EXISTS users ("
                    "id INTEGER PRIMARY KEY AUTOINCREMENT,"
                    "name TEXT NOT NULL,"
                    "age INTEGER)";

  char *err_msg = nullptr;
  defer freeError (
      [&err_msg] () -> void
        {
          if ( err_msg )
            {
              sqlite3_free ( err_msg );
            }
        } );

  const auto exec_result = sqlite3_exec ( db, sql, nullptr, nullptr, &err_msg );
  if ( exec_result != SQLITE_OK )
    {
      std::cerr << "SQL error: " << errMsg << "\n";
      return;
    }

  std::cout << "Table created successfully\n";
}

This is a great way to put the cleanup logic in the same place the creation occurs. And because a lambda can be passed to the defer constructor, you can capture anything you want to use within it!

Leave a Reply

Discover more from Nathaniel Wright

Subscribe now to keep reading and get access to the full archive.

Continue reading