Coding in C++ like it's Golang (Part 2)

Golang has some nice features such as multiple return values, the defer keyword, and channels. This article shows how to implement Golang’s defer statement in Modern C++.

Table of Contents

Golang has some mechanisms which are helpful for developing software in Cloud Computing environments. These mechanisms address challenges that are ubiquitous in cloud computing environments, such as handling concurrency scenarios or deploying programs in distributed execution contexts. Some of these mechanisms are useful for real-time scenarios as well. When porting them to C++, care must be taken to follow the principles of real-time programming. Despite these extra challenges (e.g. handling memory allocations) it is absolutely possible to achieve our goal. In this article we will show how to defer logic to be executed before a scope (function, block, etc.) is left.

We are not the first to re-implement Golang-features in C++. There are several resources that deal with achieving this goal:

Similar to Python’s try-finally mechanism, this approach can be highly advantageous for effectively managing resource cleanup or deferring actions in a structured and convenient manner.

Deferring Logic in Go

In this article, we will focus on deferring logic specifically in the context of database access scenarios. This emphasis will provide the foundation for our broader discussion on deferring specific actions to later points in time, such as finalizing access or closing the database towards the end of our process. To get started, let’s demonstrate how to create and populate an SQLite database using the sqlite3 command-line tool.

rm -f /tmp/example.db
sqlite3 /tmp/example.db <<-EOF
    CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, age INTEGER);
    INSERT INTO employees (name, age) VALUES ('John Doe', 30), ('Jane Smith', 25);
    SELECT * FROM employees;
EOF

Result:

1|John Doe|30
2|Jane Smith|25

Golang Reference Implementation

The Golang reference implementation queries the previously created and populated database using the database/sql interfaces which are implemented by mattn/go-sqlite3:

 1package main
 2
 3import (
 4	"database/sql"
 5	"fmt"
 6	"log"
 7
 8	_ "github.com/mattn/go-sqlite3"
 9)
10
11func main() {
12	db, err := sql.Open("sqlite3", "file:/tmp/example.db?mode=ro")
13	if err != nil {
14		log.Fatal("Cannot open database:", err)
15	}
16	defer db.Close()
17
18	rows, err := db.Query("SELECT * FROM employees")
19	if err != nil {
20		log.Fatal("SQL error:", err)
21	}
22	defer rows.Close()
23
24	for rows.Next() {
25		var id int
26		var name string
27		var age int
28
29		err := rows.Scan(&id, &name, &age)
30		if err != nil {
31			log.Fatal("Error scanning row:", err)
32		}
33
34		fmt.Printf("ID: %d, Name: %s, Age: %d\n", id, name, age)
35	}
36
37	if err = rows.Err(); err != nil {
38		log.Fatal("Error retrieving rows:", err)
39	}
40}

First, we open the SQLite database at /tmp/example.db in read-only mode. In case, opening the database goes well, closing the database handle is deferred to the end of the main function. After that, all fields from all rows are queried from the employees table and printed to stdout.

In Golang, the defer statement is used to schedule a function call to be executed when the surrounding function returns, either because it reaches the end of the function or encounters a panic. If defer is used multiple times in the same scope, the scheduled function calls are executed in the reverse order they were deferred.

When you defer a function call, the arguments to that function are actually evaluated at the time the defer statement is executed, not when the deferred function itself is executed (link to the documentation). This means that the code at the point of the defer statement determines the values of the arguments, and the deferred function uses those values when it is eventually called.

Running the above .go file results in:

go run database/cmd/main.go
ID: 1, Name: John Doe, Age: 30
ID: 2, Name: Jane Smith, Age: 25

Now, we will move on to implementing the defer statement in C++.

Implementation in Modern C++

As the C++ language does not have a defer statement built-in, we have to implement it ourselves through the use of RAII (Resource Acquisition Is Initialization). In C++, RAII is a common idiom used to manage resources such as memory, file handles, and network connections. Objects in C++ are automatically destroyed when they go out of scope, and their destructors are called. This behavior can be leveraged to mimic the defer functionality. By creating an object whose destructor contains the code you would like to defer, you ensure that this code is executed when the object goes out of scope.

For instance, you can define a class with a destructor that executes the desired deferred action. When an instance of this class goes out of scope, the destructor is called, and the deferred action is executed:

 1#include <iostream>
 2#include <functional>
 3
 4class Defer {
 5public:
 6    Defer(std::function<void()> f) : func(f) {}
 7    ~Defer() { func(); }
 8
 9private:
10    std::function<void()> func;
11};
12
13int main() {
14    Defer defer_example([]{ std::cout << "Deferred action executed.\n"; });
15    std::cout << "Main function body.\n";
16    // Deferred action is executed here when defer_example goes out of scope
17    return 0;
18}
g++ -std=c++20 -o raii_defer raii_defer.cpp
./raii_defer
Main function body.
Deferred action executed.

In this example, the lambda function passed to the Defer class is executed when the defer_example object is destroyed at the end of the main function’s scope. This technique effectively simulates parts of Go’s defer behavior in C++. Variables used in deferred callbacks must be captured to ensure accessibility.

For the moment, only one function can be deferred by a single object. What if we want to allow for registering multiple deferred functions? In this case, we will need a data structure capable of holding multiple such callbacks:

 1#include <array>
 2#include <functional>
 3#include <initializer_list>
 4#include <stdexcept>
 5
 6namespace htl {
 7
 8using CallbackT = std::function<void()>;
 9
10template <std::size_t stack_depth = 4> class Defer {
11public:
12  Defer() : callbacks{}, num_callbacks{0} {}
13  template <typename T, typename... Rest>
14  Defer(T arg, Rest... rest)
15      : callbacks{arg, rest...}, num_callbacks{sizeof...(Rest) + 1} {}
16  ~Defer() {
17    if (!num_callbacks) {
18      return;
19    }
20    for (auto idx = num_callbacks; idx > 0; --idx) {
21      callbacks[idx - 1]();
22    }
23  }
24  void defer(CallbackT callback) {
25    if (num_callbacks >= callbacks.max_size()) {
26      throw std::runtime_error("Number of deferred functions exceeded.");
27    }
28    callbacks[num_callbacks++] = callback;
29  }
30
31private:
32  std::array<CallbackT, stack_depth> callbacks;
33  std::size_t num_callbacks;
34};
35
36} // namespace htl

To avoid runtime memory allocations (assuming usage in real-time scenarios), the Defer class utilizes a statically sized std::array to store callbacks. The size of the array, defining the maximum number of callbacks it can accommodate, is set by the stack_depth template parameter. Deferred functions are added through the class’s variadic constructor or the defer method and are executed in the destructor in reverse order they have been registered (LIFO; Last In - First Out).

Let’s find out by porting the SQLite Golang example from above to C++. But first, we have to decide on the SQLite library to use in our C++ samples. Numerous modern C++ wrappers for SQLite are available that offer high-quality and user-friendly interfaces. Notable examples include:

However, to better illustrate the principles of our self-development, I would like to keep things simple in our practical example and stay with the original C-style API of the SQLite project:

 1#include "defer.hpp"
 2
 3#include <cstdlib>
 4#include <iostream>
 5
 6#include <sqlite3.h>
 7
 8int main() {
 9  sqlite3 *db;
10  int rc = sqlite3_open("file:/tmp/example.db?mode=ro", &db);
11  if (rc != SQLITE_OK) {
12    std::cerr << "Cannot open database: " << sqlite3_errmsg(db) << std::endl;
13    return rc;
14  }
15
16  htl::Defer deferred{[&db] { sqlite3_close(db); }};
17
18  sqlite3_stmt *stmt;
19  rc = sqlite3_prepare_v2(db, "SELECT * FROM employees;", -1, &stmt, 0);
20  if (rc != SQLITE_OK) {
21    std::cerr << "SQL error: " << sqlite3_errmsg(db) << std::endl;
22    return rc;
23  }
24
25  deferred.defer([&stmt] { sqlite3_finalize(stmt); });
26
27  while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
28    int id = sqlite3_column_int(stmt, 0);
29    const char *name =
30        reinterpret_cast<const char *>(sqlite3_column_text(stmt, 1));
31    int age = sqlite3_column_int(stmt, 2);
32
33    std::cout << "ID: " << id << ", Name: " << name << ", Age: " << age
34              << std::endl;
35  }
36
37  if (rc != SQLITE_DONE) {
38    std::cerr << "SQL error: " << sqlite3_errmsg(db) << std::endl;
39  }
40
41  return EXIT_SUCCESS;
42}

Upon opening the database, the Defer class instance, deferred, is initialized with a lambda that captures the db pointer by reference. This lambda ensures the database connection closes when deferred exits the main scope. Additionally, to manage the prepared statement stmt, its cleanup procedure is also registered with deferred. Callbacks in Defer execute in reverse order, so the statement is freed before the database connection is closed.

Thus, the Defer class efficiently handles resource cleanup for both the database and statement, guaranteeing their proper release upon exiting main, regardless of the exit path.

Running the example works as expected:

g++ -std=c++20 -o sqlite3_query sqlite3_query.cpp -lsqlite3
./sqlite3_query
ID: 1, Name: John Doe, Age: 30
ID: 2, Name: Jane Smith, Age: 25

Note that we did not take into account, that function arguments to deferred functions in Golang are evaluated when the defer statement is encountered.

Conclusion

Deferred statements in C++ offer a flexible and explicit approach to resource management, surpassing traditional methods like manual cleanup or more automatic approaches such as RAII with smart pointers. They allow cleanup code to be defined at the point of resource acquisition, enhancing code readability and maintainability, especially in complex functions with multiple resources. This approach is particularly beneficial for managing resources that can’t be easily encapsulated or require specific release calls.