I’ve been reading and writing C++ (cuCascade, RMM) and Go (Hyperledger Fabric) code recently and constantly switching between them made me think about their similarities and differences w.r.t to resource management.

There are a few major problems with manually managing memory and non-memory resources. For simplicity, in this blog, memory refers to physical RAM and a non-memory resource refers to a file handle, mutex, TCP connection etc. but they can refer to a range of resources.

  • Memory leaks: A program that requests memory on the heap but never frees it. This can eventually build up leading to OOM errors.
  • Dangling pointers (use after free): For C-style file pointers FILE* fd , using the pointer after the file is closed with fclose(fd) can lead to undefined behaviour.
  • Double free: This occurs when the program tries to free the same memory twice and also leads to undefined behaviour.

All of these issues can arise when writing C++. Go has a garbage collector, so manual heap memory management is not an issue while leaks can still occur if resources like TCP connections, file handles are not closed.

Bad resource management can go unnoticed or have severe consequences. If your program allocates a few hundred bytes on the heap but does not free it, it can go unnoticed. Resources like file descriptors are more constrained especially in Linux where regular files, socket connections and inter-process communication using pipes all use file descriptors. The default soft limit for these is 1024. On a high-concurrency web server, the system can run out of file descriptors when you need it the most i.e. high traffic for your application. So it is important to close file descriptors after you’re done using them. Concurrency bugs due to acquired mutexes not being released can affect correctness and liveness.

C++ Resource Management

C++ Core Guidelines discourage explicitly calling new and delete. If you allocate memory using new , your program might have a bunch of delete s all over the place as it can take many paths like calling helper functions, returning early due to exceptions, conditionals etc. new returns a pointer for managing the object. A pointer by itself is non-owning. You can copy a pointer, use it after freeing the resource, and copies can lead to calling delete more than once.

#include<iostream>

int main() {

  int* x = new int;
  *x = 10;
  std::cout << *x << std::endl;

  int* y = x; // x is non-owning; y now points to the same memory location
  std::cout << *y << std::endl; // use y

  delete x;
  delete y; // double delete; who's responsible for freeing the memory?

}

Compiling and running the program with g++ -fsanitize=address main.cpp returns the following error:

Double Free error


Resource Acquisition Is Initialization (RAII) is a programming idiom that aims to solve this problem by binding the life cycle of a resource to the lifetime of an object. Resources that must be acquired before use and later released, like heap memory, mutexes, file handles have a symmetry (new then delete , lock then unlock , fopen then fclose). This mirrors the constructor/destructor life cycle of an object. Whenever there is a requirement for such a resource, encapsulate this functionality into an object by acquiring the resource in the constructor and releasing it in the destructor.

Consider an object of class Foo allocated on the heap with new as follows:

#include<iostream>

class Foo {

  public:

  void doSomething() {
    std::cout << "Doing something..." << std::endl;
    std::cout << "Done" << std::endl;
  }

};

void doWork() {
  Foo* foo = new Foo();
  foo->doSomething();
}

int main() {
  
  doWork();

  return 0;

}

This might look simple as the object is only used in the function doWork() . But doWork() itself can call other functions and pass foo, throw exceptions, have conditional statements. Is there a way to de-allocate memory used by foo after the function ends? The answer lies in destructors and lifetime of local (stack) variables.

Destructors are invoked when the lifetime of an object ends. For objects allocated with new (heap), that happens when you explicitly call delete . For a local (stack) variable, the lifetime ends in the closing brace } of the block in which the variable was created and the destructor is called automatically.

Is there a way to bind the lifetime of the resource to a local variable so that it is freed after the block/function ends? The answer lies in using a local variable (which has automatic storage duration) and acquiring the resource in the constructor and releasing it in the destructor. C++ provides this convenience through the class std::unique_ptr. Rewriting the above program:

#include<iostream>
#include<memory>

class Foo {

  public:

  void doSomething() {
    std::cout << "Doing something..." << std::endl;
    std::cout << "Done" << std::endl;
  }

};

void doWork() {
  std::unique_ptr<Foo> foo = std::make_unique<Foo>();
  foo->doSomething();
}

int main() {
  
  doWork();

  return 0;

}

Here foo is a local variable that holds a dynamically allocated resource of class Foo (allocated in the constructor of std::unique_ptr). When the function doWork() ends, foo ’s lifetime ends, its destructor is invoked and the resource is freed.

The most important characteristic is that foo is an owning pointer. It is responsible for creating the object and destroying it. Attempting to copy it to another pointer will produce an error:

std::unique_ptr<Foo> foo = std::make_unique<Foo>();
std::unique_ptr<Foo> foo2 = foo;

Unique Pointer copy

The copy constructor for std::unique_ptr<T> is deleted. Allowing copy would introduce the problems mentioned before about ownership (who’s responsible for deletion?). But the move constructor exists and ownership can be transferred through std::move .

std::unique_ptr<Foo> foo = std::make_unique<Foo>();
std::unique_ptr<Foo> foo2 = std::move(foo); 
// foo2 holds the object Foo; it is now responsible for destroying it
// foo holds nullptr


Mutex acquisition RAII

With mutexes, we encounter similar problems:

void bar(){
	mtx.lock();
	if(condition) {
		// might return early
	}
	mtx.unlock();
}

Think about how we would write a RAII mechanism for acquiring and releasing mutexes. The mutex needs to be acquired in the constructor and released in the destructor. Here “ownership” of the mutex means managing its locking/unlocking. We need to delete the copy constructors of the class managing this lock/unlock to enforce ownership. One implementation could look like this:

#include<iostream>
#include<memory>
#include<mutex>

class MutexGuard{

  public:
    MutexGuard(std::mutex& mtx) : mtx(mtx) {
      mtx.lock();
    }

    ~MutexGuard() {
      mtx.unlock();
    }

    MutexGuard(const MutexGuard&) = delete;
    MutexGuard& operator=(const MutexGuard&) = delete;

  private:
    std::mutex& mtx;
};

class Foo {

  public:

  void doSomething() {
    MutexGuard guard(mtx);
    std::cout << "Doing something..." << std::endl;
    std::cout << "Done" << std::endl;
  }

  private:

  std::mutex mtx;

};

void doWork() {
  std::unique_ptr<Foo> foo = std::make_unique<Foo>();
  foo->doSomething();
}

int main() {
  
  doWork();

  return 0;

}

When the guard variable is created, the mutex is acquired and at the end of the function, the variable’s lifetime ends, and the destructor is invoked which unlocks the mutex.

void bar(){
	MutexGuard guard(mtx);
	if(condition) {
		// the mtx is unlocked when the function ends
		// in all possible code paths because guard's lifetime ends
	}
}

The standard library provides a class called std::lock_guard that provides the same functionality.

Go Resource Management

Go does not have constructors (although you can attempt to write one) or destructors and the Garbage Collector identifies memory that is no longer used by the application and recycles it. But there is a need to close files, TCP connections and unlock mutexes after acquiring them to prevent resources leaks. The idiomatic way to do this which resembles RAII is by using a defer statement. A typical usage of defer looks like this:

func readsFile(fileName string) {
	file, err := os.Open(fileName)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()
	
	content, err := io.ReadAll(file)
	if err != nil {
		log.Fatal(err)
	}
	
	fmt.Println(string(content))
}

A defer statement usually follows a successful acquisition of resource, in this case, when the file is successfully opened. When control reaches the defer statement, the arguments of the function call are immediately evaluated. But the actual call is executed when when the surrounding function returns. If there are multiple statements, they are pushed onto a stack and executed in last-in-first-out order.

An important distinction between Go and C++ RAII is the point at which the resource is released. In C++, the destructor is invoked when the variable’s lifetime ends which is the end of the block. In Go, the defer statement’s call runs when the surrounding function returns. Thus, a C++ RAII resource (where the resource is owned by a local variable) has block-level scope while Go’s acquistion/ defer pair has function-level scope.

if condition {
	mtx.Lock()
	defer mtx.Unlock()  // The mutex is released when the surrounding function returns
	// Usage
}

if (condition) {
	std::lock_guard<std::mutex> lck(mtx); // The mutex is released at the end of this block
	// Usage
}

This has an important implication. 100 Go Mistakes and How to Avoid Them illustrates this effectively. When using defer in a loop, a long-running or infinite loop can cause problems by not releasing resources until the surrounding function returns.

If you prefer Go’s way of doing things, you’re not alone.

Please reach out if you find any errors.