Destructors and objects as keeper of a resource

Usually, C++ is not used in conjunction with a so-called garbage collection that automatically deallocates storage which can no longer be reached. Instead, we should deallocate no longer required space explicitly which has been allocated before using the new operator. C++ provides us with a delete operator which does this. If we forget to deallocate no longer accessible memory, we have a memory leak which can be a serious problem for any long-running application.

But who is responsible for deallocating dynamic memory like that used for a matrix? In more general terms we like to speak of resources which at some point need to be acquired and eventually released. Forgetting to release a resource can be serious problem. Imagine a lock which has been acquired but then forgotten -- this may lead to a deadlock.

In C++ the responsibility for a resource is preferably tied to an object where

The former can be done (as already demonstrated) in the constructor, for the latter we have so-called destructor methods in C++.

This approach has a name: resource acquisition is initialization (RAII). This technique was developed for C++ but also meanwhile adapted elsewhere. This approach makes sure that a resource is not accidently “forgotten”. In addition, it provides us also with exception safeness, i.e. the resource is properly released even if some exception causes the stack to be unwound along with local variables that have acquired resources.

struct Matrix {
   const std::size_t m; /* number of rows */
   const std::size_t n; /* number of columns */
   const std::size_t incRow;
   const std::size_t incCol;
   double* data;

   Matrix(std::size_t m, std::size_t n, StorageOrder order) :
         m(m), n(n),
         incRow(order == StorageOrder::ColMajor? 1: n),
         incCol(order == StorageOrder::RowMajor? 1: m),
         data(new double[m*n]) {
   }
   ~Matrix() {
      delete[] data;
   }

   /* ... */
};

Similar to constructors, destructors use the name of the class but they are prefixed with a tilde (i.e. ~). They have no parameters. As the new operator was used for an array using [], the delete operator requires the brackets as well.

The time when the destructor is invoked depends on how the object has been allocated:

In summary, destructors are guaranteed to be invoked at well-defined moments. Thereby they are well suited to release resources that are no longer needed.

This approach is, however, not coming without dangers. Whenever a pointer or a reference to a resource is leaked, it is possibly used after the object holding it is destructed. Any later attempt to access an already released resource (like deallocated memory) is undefined and may end as security vulnerability. Hence, we must be careful to restrict the access to these resources or be otherwise sure that any intended leak is not subsequently misused by continuing to use it after it has been released or by releasing it twice.

Exercise

Extend the destructor with some output for testing purposes and test this for all four scenarios above, i.e. by constructing objects that are global, local, dynamically allocated, and temporary.

Example

#include <cassert> /* needed for assert */
#include <cstddef> /* needed for std::size_t and std::ptrdiff_t */
#include <printf.hpp> /* needed for fmt::printf */

enum class StorageOrder {ColMajor, RowMajor};

struct Matrix {
   const std::size_t m; /* number of rows */
   const std::size_t n; /* number of columns */
   const std::ptrdiff_t incRow;
   const std::ptrdiff_t incCol;
   double* data;

   Matrix(std::size_t m, std::size_t n, StorageOrder order) :
         m(m), n(n),
         incRow(order == StorageOrder::ColMajor? 1: n),
         incCol(order == StorageOrder::RowMajor? 1: m),
         data(new double[m*n]) {
   }

   ~Matrix() {
      delete[] data;
   }

   const double& operator()(std::size_t i, std::size_t j) const {
      assert(i < m && j < n);
      return data[i*incRow + j*incCol];
   }
   
   double& operator()(std::size_t i, std::size_t j) {
      assert(i < m && j < n);
      return data[i*incRow + j*incCol];
   }

   void init() {
      for (std::size_t i = 0; i < m; ++i) {
         for (std::size_t j = 0; j < n; ++j) {
            data[i*incRow + j*incCol] = j * m + i + 1;
         }
      }
   }

   void print() {
      for (std::size_t i = 0; i < m; ++i) {
         fmt::printf("  ");
         for (std::size_t j = 0; j < n; ++j) {
            fmt::printf(" %4.1lf", data[i*incRow + j*incCol]);
         }
         fmt::printf("\n");
      }
   }
};

int main() {
   Matrix A(7, 8, StorageOrder::ColMajor);
   A.init();
   fmt::printf("A =\n"); A.print();
}