C++ Core Guidelines: Rules to Resource Management

This and the next posts will probably be about the most important concern in programming: resource management. The C++ Core Guidelines has rules for resource management in general but also rules for allocation and deallocation and smart pointers in particular. Today I will begin with the general rules of resource management.

 

photo montage 1548599 640

At first. What is a resource? A resource is something which you have to manage. That means you have to acquire and release it because resources are limited or you have to protect them. You can only have a limited amount of memory, sockets, processes or threads; only one process can write a shared file or one thread can write a shared variable at one point in time. If you don't follow the protocol, a lot of issues are possible.

Your system may

  • become out of memory because you leak memory.
  • have a data race because you forget to acquire a lock before you use the shared variable.
  • have a deadlock because you are acquire and release a few shared variables in a different sequence.

The issues with data race and data locks are not unique to shared variables. For example, you can have the same issues with files.

If you think about resource management it all boils down to one key point: ownership. So let me give you first the big picture before I write about the rules.

What I like in particular about modern C++, is, that we can directly express our intention about ownership in code.

  • Local objects. The C++ runtime as the owner automatically manages the lifetime of these resources. The same holds for global objects or members of a class. The guidelines calls them scoped objects.
  • References: I'm not the owner. I only borrowed the resource that cannot be empty.
  • Raw pointers: I'm not the owner. I only borrowed the resource that can be can be empty. I must not delete the resource.
  • std::unique_ptr: I'm the exclusive owner of the resource. I may explicitly release the resource.
  • std::shared_ptr: I share the resource with other shared ptr. I may explicitly release my shared ownership.
  • std::weak_ptr: I'm not the owner of the resource but I may become temporary the shared owner of the resource by using the method std::weak_ptr::lock.

Compare this fine-grained ownership semantic to just a raw pointer. Now you know, what I like about modern C++.

Here is the summary of the rules to resource management.

Let's look at each of them in detail.

R.1: Manage resources automatically using resource handles and RAII (Resource Acquisition Is Initialization)

The idea is quite simple. You create a kind of a proxy object for your resource. The constructor of the proxy will acquire the resource and the destructor will release the resource. The key idea of RAII is that the C++ runtime is the owner of the local object and therefore of the resource.

Two typical examples for RAII in modern C++ are smart pointers and locks. Smart pointer takes care of their memory and locks take care of their mutexes.

The following class ResourceGuard models RAII.

// raii.cpp

#include <iostream>
#include <new>
#include <string>

class ResourceGuard{
  private:
    const std::string resource;
  public:
    ResourceGuard(const std::string& res):resource(res){
      std::cout << "Acquire the " << resource << "." <<  std::endl;
    }
    ~ResourceGuard(){
      std::cout << "Release the "<< resource << "." << std::endl;
    }
};

int main(){

  std::cout << std::endl;

  ResourceGuard resGuard1{"memoryBlock1"};                  // (1)

  std::cout << "\nBefore local scope" << std::endl;
  {
    ResourceGuard resGuard2{"memoryBlock2"};                // (2)
  }
  std::cout << "After local scope" << std::endl;
  
  std::cout << std::endl;

  
  std::cout << "\nBefore try-catch block" << std::endl;
  try{
      ResourceGuard resGuard3{"memoryBlock3"};              // (3)
      throw std::bad_alloc();
  }   
  catch (std::bad_alloc& e){
      std::cout << e.what();
  }
  std::cout << "\nAfter try-catch block" << std::endl;
  
  std::cout << std::endl;

}

 

It makes no difference, if the lifetime of instances of ResourceGuard ends regularly (1) and (2) or irregularly (3). The destructor of ResourceGuard will always be called. This means the resource will be released.

raii

If you want to know more details about the example and RAII, read my post: Garbage Collection - No Thanks. Even Bjarne Stroustrup made a comment.

R.2: In interfaces, use raw pointers to denote individual objects (only)

Raw pointers should not denote arrays because this is very error-prone. This becomes, in particular, true if your function takes a pointer as an argument.

void f(int* p, int n)   // n is the number of elements in p[]
{
    // ...
    p[2] = 7;   // bad: subscript raw pointer
    // ...
}

 

It's quite easy to pass the wrong size of the array as an argument.

For arrays, we have containers such as std::vector. A container of the Standard Template Library is an exclusive owner. It acquires and releases it's memory automatically.

R.3: A raw pointer (a T*) is non-owning

The issue of ownership becomes in particular interesting if you have a factory. A factory is a special function that returns a new object. Now the question is. Should you return a raw pointer, an object, an std::unique_ptr, or an std::shared_ptr?

Here are the four variations:

Widget* makeWidget(int n){                    // (1)
    auto p = new Widget{n};
    // ...
    return p;
}

Widget makeWidget(int n){                     // (2)
    Widget g{n};
    // ...
    return g;
}

std::unique_ptr<Widget> makeWidget(int n){    // (3)
    auto u = std::make_unique<Widget>(n);
    // ...
   return u;
}

std::shared_ptr<Widget> makeWidget(int n){    // (4)
    auto s = std::make_shared<Widget>(n);
    // ...
   return s;
}

...

auto widget = makeWidget(10);

 

Who should be the owner of the widget? The caller or the callee? I assume you can not answer the question for the pointer in the example. Me too. This means we have not idea who should call delete. In contrast, the cases (2) to (4) are quite obvious. In the case of the object or of the std::unique_ptr, the caller is the owner. In case of the std::shared_ptr, the caller and the callee share the ownership.

One question remains. Should you go with an object or a smart pointer. Here are my thoughts.

  • If your factory must be polymorphic such as a virtual constructor, you have to use a smart pointer. I have already written about this special use-case. Read the details in the post: C++ Core Guidelines: Constructors (C.50).
  • If the object is cheap to copy and the caller should be the owner of the widget, use an object. If not cheap to copy, use an std::unique_ptr.
  • If the callee wants to manage the lifetime of the widget, use an std::shared_ptr

R.4: A raw reference (a T&) is non-owning

There is nothing to add. A raw reference is non-owning and cannot be empty.

R.5: Prefer scoped objects, don’t heap-allocate unnecessarily

A scoped object is an object with its own scope. That may be a local object, a global object, or a member. The C++ runtime takes care of the object. There is no memory allocation and deallocation involved and we can not get an std::bad_alloc exception. To make it simple. If possible, use a scoped object.

R.6: Avoid non-const global variables

I often hear: global variables are bad. That is not totally true. Non-const global variables are bad. There are many reasons for avoiding non-const global variables. Here are a few reasons. I assume for simplicity reasons that the functions or objects use non-const global variables.

  • Encapsulation: Functions or objects could be changed outside of their scope. This means it is quite difficult to think about your code.
  • Testability: You can not test your function in isolation. The effect of your function depends on the state of your program.
  • Refactoring: It's quite difficult to refactor your code if you can not think about your function in isolation.
  • Optimisation: You can not easily rearrange the function invocations or perform the function invocations on different threads because there may be hidden dependencies.
  • Concurrency: The necessary condition for having a data race is shared, mutable state. Non-const global variables are shared mutable state.

What's next?

In the next post, I will write about a very important resource: memory.

 

Thanks a lot to my Patreon Supporters: Eric Pederson, Paul Baxter, and Franco Amato.

 

Get your e-book at leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11, C++14, and C++17 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

 

Get my books "The C++ Standard Library" (including C++17) and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 550 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

Add comment


My Newest E-Books

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 873

All 538594

Currently are 175 guests and no members online