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
Introduction and Related Work
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.