C++ Core Guidelines: Rules about Resource Management
This and the following posts will probably be about the most critical 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.
At first. What is a resource? A resource is something that you have to manage. That means you must acquire and release it because resources are limited, or you must 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. If you don’t follow the protocol, many 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 resources 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 may temporarily become the shared owner 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++.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
Here is a summary of the rules for resource management.
- R.1: Manage resources automatically using resource handles and RAII (Resource Acquisition Is Initialization)
- R.2: In interfaces, use raw pointers to denote individual objects (only)
- R.3: A raw pointer (a
T*
) is non-owning - R.4: A raw reference (a
T&
) is non-owning - R.5: Prefer scoped objects, don’t heap-allocate unnecessarily
- R.6: Avoid non-
const
global variables
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 proxy’s constructor 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 care for 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.
Read my post: Garbage Collection – No Thanks for more details about the example and RAII. 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 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 apparent. In the case of the object or 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 is polymorphic, such as a virtual constructor, you must 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. No memory allocation and deallocation are 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 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 pretty challenging 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: Refactoring your code is pretty challenging 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 states.
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, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, 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, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!