Garbage Collection - No Thanks

Contents[Show]

C++ is old fashioned. C++ has no garbage collection. No garbage collection? Right! Old fashioned? Wrong!

What is against garbage collection in C++? At first, garbage collection breaks one of the key principles of C++: "Don't pay for something you don't use." That means if you don't need garbage collection your C++ runtime should not waste its time cleaning up the whole garbage. My second point is more sophisticated.

We have RAII in C++ and therefore the totally deterministic destruction of objects. But, what is RAII? That's the topic of this post.

Resource Acquisition Is Initialization

RAII stand for Resource Acquisition Is Initialization. Probably, the most important idiom in C++ says that a resource should be acquired in the constructor of the object and released in the destructor of the object. The key is that the destructor will be automatically called if the object goes out of scope. If this is not totally deterministic? In Java or Python (__del__) you have a destructor but not the guarantee. Therefore, it can end disastrous, if you use the destructor to release a critical resource like a lock. That's a classical anti-pattern for a deadlock. But in C++, we are on the safe side.

The example shows the totally deterministic behaviour of RAII in C++.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 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"};

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

  
  std::cout << "\nBefore try-catch block" << std::endl;
  try{
      ResourceGuard resGuard3{"memoryBlock3"};
      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;

}

 

ResourceGuard is a guard that managed its resource. In this case, the resource is a simple string. ResourceGuard creates in its constructor (line 11 - 13) the resource and releases the resource in its destructor (line 14 - 16). It does its job very reliable.

The destructor of resGuard1 (line 23) will be exactly called at the end of the main function (line 46). The lifetime of resGuard2 (line 27) already ends in line 28. Therefore, the destructor will automatically be executed. Even the throwing of an exception does not alter the reliability of resGuard3 (line 36). Its destructor will be called at the end of the try block (line 35 - 38).

The screenshot displays the lifetime of the objects.

raii

I will refer once more to the program raii.cpp. What is the key idea of the RAII idiom? The lifetime of a resource will be bound to the lifetime of a local variable. C++ automatically manages the lifetime of locals.

The idea is quite simple but has far-reaching consequences. Critical resources are bound to local objects. The remaining job is done by the C++ runtime.

RAII everywhere

RAII everywhere holds for locks  std::lock_guard, std::unique_lock, and std::shared_lock  that manage their resource mutex. RAII everywhere holds for the smart pointers std::unique_ptr, std::shared_ptr, and std::weak_ptr that manage their resource memory.

Thanks to RAII we have in C++ a kind of garbage collection.

But there is a subtle difference to a general garbage collection.

  1. You have to explicitly use smart pointers in C++
  2. The memory management with  std::unique_ptr has by design no overhead in performance or memory compared to a raw pointer (see Memory and Performance Overhead of Smart Pointers).

Therefore, C++ adhere to its key principle in a twofold way: Don't pay for something you don't use.

Special resources

Thanks to RAII, the destructor of the local object and therefore the cleaning up of the resource will be done totally deterministic. So far, so good. If the destructor can throw an exception, the destructor of the object modelling RAII will trigger the exception. This will be the case if the resource is a file. The close function can trigger an exception. Therefore, you have to answer the question for yourself, if it's ok, that the destructor can throw an exception. If not, you should not use RAII.

Dealing with throwing destructors (Udo Steinbach)

Udo Steinbach wrote an e-mail to me about the issue with potentially throwing destructors. Here is the e-mail.

RAII is a nice thing − as long as no error can occur while destroying the object. The latter is forgotten when talking about RAII. Why a destructor should not throw, you can read in many places: https://www.qwant.com/?q=should%20destructors%20throw. As a result, RAII has to be supplemented manually in many cases and so it seems to be done twice.

 

class MyFileHandle
{  public:
      MyFileHandle(...)
         :handle(::OpenFile(...))
      {  if (handle == nullptr)
            throw ...;
      }
      ~MyFileHandle() noexcept
      {  ::CloseFile(handle);
      }
   private:
      MySystemHandle handle;
};


{
   MyFileHandle file(...);
   ...
}

Does CloseFile() not close, the call looks correct but the handle is lost, the user has to restart the program and search and delete the file himself, or other embarrassing symptoms as they are familiar from programs.

So the class must have a throwing

void Close();

and the destructor has to check:

{ 
MyFileHandle file(...)
...
file.Close();
}

This doesn't look like pure RAII. For symmetry we need a manual Open():

{
MyFileHandle file;
file.Open(...);
...
file.Close();
}

 

− RAII is lost. For the lover remains the consolation, that the object is now reusable and runs correctly in both non-error and error case.

Under the condition of proper error handling from the perspective of the program user, I renounce RAII in many of my classes. Automatic tests according to an idea from boost show

  • exception safety as a minimum,

  • with best-known error handling, which must be dispensed with at RAII, e.g. automatically delete incomplete files and rethrow the exception,

  • and messages presentable to the user,

an always best possible behaviour of the program: Make users and support happy by replacing crash and data garbage with meaningful messages. An automatism, here destructor or garbage collector, can handle errors only automatically: on a minimalistic scale or ignore it completely.

In application programs this should not be accepted nor should it be.

The famous last words (Bjarne Stroustrup)

BjarneStroustrup

 

Bjarne Stroustrup made a short remark to my news about this post on C++Enthusiasts:

"Things are still improving": http://www.stroustrup.com/resource-model.pdf

 

So what is this 20 pages paper about, written by Bjarne Stroustrup, Herb Sutter, and Gabriel Dos Reis. I provide you with a screenshot of the abstract and the overview. You have to read the very readable paper by yourself.

paper

 

I promise I will write in future posts about the Core C++ Guidelines. But, you have to be patient. I first have to catch up with my German blog.

What's next?

With the next post, I enter the area of the C++ experts. I write about explicit memory management in C++.

 

 

 

 

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Louis St-Amour, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Tobi Heideman, Daniel Hufschläger, Red Trip, Alexander Schwarz, Tornike Porchxidze, Alessandro Pezzato, Evangelos Denaxas, Bob Perry, Satish Vangipuram, and Andi Ireland.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, and Said Mert Turkal.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. 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.

New

Contact Me

Modernes C++,

RainerGrimmSmall

Tags: memory

Comments   

0 #1 Mark Abraham 2017-01-07 01:58
I think the pattern of doing work in the constructor like OpenFile is already doubtful, compared with passing in the constructed stream (which is also more suitable for testing). It gets worse when by so doing you propagate the problem of throwing clean-up code in a non-explicit way. If client code anyway has to call a close method, let them do so on the stream and keep it simpler by building a component that itself is exception safe.
Quote
0 #2 Zbigniew 2017-07-21 13:58
Your example is a good argument against garbage collector. In other languages this kind of resources requires kind of IDisposable.
Lack of GC force the developer to thing about the design. Example of this could be multithred/agent application which tasks exchange data by pointers. So developer needs to think who will take responsibility of orphaned task, especially if we assume that task can close itself or it can be closed by other. From my perspective this forced design allows better re-use of components.
Quote
0 #3 Gregg Wonderly 2020-12-28 18:05
One of the problem beliefs is that a developer should not have to worry about asynchronous behaviors. The problem with this detail, is that it's not true anywhere in life. People run into you walking on the sidewalk, you learn to dodge them as best you can. Distracted drivers hit you in your vehicle because they missed an asynchronous event to their thought processes.

In the end, programming languages have to deal with asynchronous events and developers have to know how to do this in a way that works. The no-acquires-in-your-constructor rule means that every action that acquires another object reference or somehow is part of another objects life cycle should be an explicit part of your API design after construction. In the class shown here, MyFileHandle should explicitly acquire the reference from ::OpenFile in an API that returns the failure explicitly as a documented part of the API. Whether it's an exception or a return value, there should be explicit steps involved in managing communication of the failure to open.

This means that each API should involve at most one acquire (use a more complex object that you wrap multiple steps into if you need that) or event failure.

Bad software design is what creates the majority of these problems and heated discussions.
Quote
0 #4 Gregg Wonderly 2020-12-28 18:14
Java is better at life-cycle management because of garbage collection. Java GC design anticipates orders of magnitudes larger counts of short lived objects (stack like allocations) and longer lived references are tucked away for less traversal because of this. GC still requires that references be maintained, and thus reference hierarchies still allow smaller overheads to having a garbage collector due to migration toward long term storage.

The beauty of Java GC is that it doesn't require you to change your coding style just because you declare something locally, but pass it to some other object to manage the lifecycle thereof. Thus, dependency injection is readily done without value vs pointer vs reference code syntax and lots of other such details.

Better yet, Java eliminates uninitialized data references since all class members are initialized to their default value whether you do it explicitly or not. That's the #1 benefit to software construction it has.

The RuntimeException class augments the Throwable class with the ability to not worry about exceptions that may occur due to null pointers or NaN values etc. Whereas the Exception class extends Throwable with the ability to declare explicitly and demand handling of error types which are not handled anywhere else and must be dealt with by the developer.

MyFileHandle would be forced to throw IOException in the constructor. The developer would deal with the constructor not working.

Java Exceptions provide the best way to make sure that developers are told about the wrong things they are doing with error handling. Unfortunately, most people believe that explicit error declarations are too much of a problem because they don't understand how and when asynchronous events can happen in software interfacing with the outside world.
Quote

My Newest E-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

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 757

Yesterday 7436

Week 30462

Month 170669

All 6610750

Currently are 170 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments