Beginner's Guide
    First Steps
    Input & Output
    Custom Types – Part 1
    Diagnostics
    Standard Library – Part 1
    Function Objects
    Standard Library – Part 2
    Code Organization
    Custom Types – Part 2
    Generic Programming
    Memory Management
    Software Design Basics

    Destructors Destructors Destructors

    Special Member Functions

    T::T() default constructor runs when new T object is created
    T::T(param…) special constructor runs when new T object is created with argument(s)
    T::~T() destructor runs when existing T object is destroyed

    The compiler generates a default constructor and a destructor if we don't define them ourselves.

    There are a four more special members that can be used to control the copying and moving behavior of a type that we will learn about in later chapters.

    • copy constructor  T::T(T const&)
    • copy assignment operator  T& T::operator = (T const&)
    • move constructor  T::T(T &&)
    • move assignment operator  T& T::operator = (T &&)

    They are also automatically generated by the compiler and don't need to be user-defined in many/most cases.

    User-Defined Constructor & Destructor

    class Point {  };
    class Test {
      std::vector<Point> w_;
      std::vector<int> v_;
      int i_ = 0;
    public:
      Test() {     std::cout << "constructor\n";   }
      ~Test() {     std::cout << "destructor\n";   }
      // more member functions …
    };
    
    if () {
      
      Test x;  // prints 'constructor'
      
    }  // prints 'destructor'

    Execution Order on Destruction

    After the destructor body has run the destructors of all data members are executed in reverse declaration order. This happens automatically and cannot be changed (at least not easily - this is C++ after all and there is of course a way to circumvent almost anything) .

    x goes out of scope → executes ~Test():

    • std::cout << "destructor\n";
    • x's data members are destroyed:
      • i_ is destroyed (fundamental types don't have a destructor)
      • v_ is destroyed → executes destructor ~vector<int>():
        • vector destroys int elements in its buffer; (fundamental type → no destructor)
        • deallocates buffer on the heap
        • v_'s remaining data members are destroyed
      • w_ is destroyed → executes destructor ~vector<Point>():
        • vector destroys Point elements in its buffer
        • each ~Point() destructor is executed
        • deallocates buffer on the heap
        • w_'s remaining data members are destroyed

    RAII

    Resource Acquisition Is Initialization

    • object construction: acquire resource
    • object destruction: release resource

    Each vector object is owner of a separate buffer on the heap where the actual content is stored.

    This buffer is allocated on demand and de-allocated if the vector object is destroyed.

    vector<int> v {0,1,2,3,4};

    vector of ints memory layout

    An object is said to be an owner of a resource (memory, file handle, connection, thread, lock, …) if it is responsible for its lifetime (initialization/creation, finalization/destruction).

    = variables refer to objects themselves, i.e., they are not just references/pointers

    This is the default behavior for fundamental types (int, double, etc.) in almost all programming languages and also the default for user-defined types in C++:

    • deep copying: produces a new, independent object; object (member) values are copied
    • deep assignment: makes value of target equal to that of source object
    • deep ownership: member variables refer to objects with same lifetime as containing object
    • value-based comparison: variables compare equal/less/… if their values are equal/less/…

    Because the lifetime of members is tied to its containing object, there is no need for a garbage collector.

    Example: Resource Handler

    We need to use an external (C) library that does its own resource management. Such resources can be memory, but also devices, network connections, opened files, etc.

    In such libraries, resources are usually handled with pairs of initialization and finalization functions, e.g. lib_init() and lib_finalize() that the user has to call.

    It's common to forget to call the finalization functions, especially when the program is large and its control flow is complicated. This can lead to hung-up devices, memory not beeing freed, etc.

    • call initialization function in constructor
    • call finalization function in destructor
    • additional advantage: wrapper class can also be used to store context information like connection details, device ids, etc. that are only valid between initialization and finalization
    • such a wrapper should in most cases be made non-copyable since it handles unique resources (explained in more detail in later chapters)
    #include <gpulib.h>
    
    class GPUContext {
      int gpuid_;
    public:
      explicit
      GPUContext (int gpuid = 0): gpuid_{gpuid} {
        gpulib_init(gpuid_);
      }
      ~GPUContext () {
        gpulib_finalize(gpuid_);
      }
      [[nodiscard]] int gpu_id () const noexcept { 
        return gpuid_;
      }
    // make non-copyable:
      GPUContext (GPUContext const&) = delete;
      GPUContext& operator = (GPUContext const&) = delete;
    };
    
    int main () {
      
      if () {
         // create/initialize context
        GPUContext gpu;
         // do something with it
        
      } // automatically finalized!
      
    }

    Example: RAII Logging

    • constructor of Device gets pointer to a UsageLog object
    • UsageLog can be used to record actions during a Device object's lifetime
    • destructor informs UsageLog if a Device is no longer present
    • the UsageLog could also count the number of active devices, etc.
    class File {  };
    class DeviceID {  };
    
    class UsageLog {
    public:
      explicit UsageLog (File const&);
      
      void armed (DeviceID);
      void disarmed (DeviceID);
      void fired (DeviceID);
    };
    
    class Device {
      DeviceID id_;
      UsageLog* log_;
      
    public:
      explicit
      Device (DeviceId id, UsageLog* log = nullptr): 
        id_{id}, log_{log}, 
      { 
        if (log_) log_->armed(id_);
      }
      ~Device () { if (log_) log_->disarmed(id_); }
      void fire () {
        
        if (log_) log_->fired(id_);
      }
      
    };
    int main () {
      File file {"log.txt"}
      UsageLog log {file};
      
      Device d1 {DeviceID{1}, &log};
      d1.fire(); 
      {
        Device d2 {DeviceID{2}, &log};
        d2.fire(); 
      }
      d1.fire(); 
    }
    log.txt
    device 1   armed
    device 1   fired
    device 2   armed
    device 2   fired
    device 2   disarmed
    device 1   fired
    device 1   disarmed

    The Rule of Zero

    = (try to) write zero special member functions

    Avoid writing special member functions unless you need to do RAII-style resource management or lifetime-based tracking.

    The compiler generated default constructor and destructor are sufficient in most cases.

    Initialization doesn't always require writing constructors.

    Most data members can be initialized with Member Initializers .

    Do not add empty destructors to types!

    The presence of a user-defined destructor prevents many optimizations and can seriously impact performance!

    If you don't need to do anything in a destructor body, then don't define one!

    You almost never need to write destructors.

    Before C++11 custom classes with explicit manual memory management were very common. However, in modern C++ memory management strategies are mostly (and should be) encapsulated in dedicated classes (containers, smart pointers, allocators, …).