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++.
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-
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.
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.
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.
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.
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:
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
There is nothing to add. A raw reference is non-owning and cannot be empty.
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.
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.
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, 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, Rob North, Bhavith C Achar, and Marco Parri Empoli.
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|
I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.
- Embedded Programmierung mit modernem C++ 12.12.2023 – 14.12.2023 (Präsenzschulung, Termingarantie)
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++
- Clean Code with Modern C++
- Phone: +49 7472 917441
- Mobil:: +49 176 5506 5086
- Mail: schulung@ModernesCpp.de
- German Seminar Page: www.ModernesCpp.de
- Mentoring Page: www.ModernesCpp.org
Modernes C++ Mentoring,