C++ Core Guidelines: Rules about Resource Management

Contents[Show]

This and the next posts will probably be about the most important concern in programming: resource management. The C++ Core Guidelines have 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 that 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 acquiring and releasing 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 call 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 for 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 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 of 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.

 

Rainer D 6 P2 540x540Modernes C++ Mentoring

Stay informed about my mentoring programs.

 

 

Subscribe via E-Mail.

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 side 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 its 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, a std::unique_ptr, or a 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 no idea who should call delete. In contrast, 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 the 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 a std::unique_ptr.
  • If the callee wants to manage the lifetime of the widget, use a 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 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 a 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.
  • Optimization: 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 a 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: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, and Dominik Vošček.

 

Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, and Slavko Radman.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

My special thanks to PVS-Studio PVC Logo

 

My special thanks to Tipi.build tipi.build logo

Seminars

I'm happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

Bookable (Online)

German

Standard Seminars (English/German)

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

  • C++ - The Core Language
  • C++ - The Standard Library
  • C++ - Compact
  • C++11 and C++14
  • Concurrency with Modern C++
  • Design Pattern and Architectural Pattern with C++
  • Embedded Programming with Modern C++
  • Generic Programming (Templates) with C++

New

  • Clean Code with Modern C++
  • C++20

Contact Me

Modernes C++,

RainerGrimmDunkelBlauSmall

Tags: Memory

Mentoring

Stay Informed about my Mentoring

 

English Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Interactive Course: The All-in-One Guide to C++20

Subscribe to the newsletter (+ pdf bundle)

All tags

Blog archive

Source Code

Visitors

Today 4152

Yesterday 7411

Week 27380

Month 171551

All 11652705

Currently are 211 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments