Destructors Destructors Destructors
Special Member
sT::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
#include <iostream>
#include <vector>
class Point { int x; int y;… };
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 …
};
int main () {
std::cout << "before\n";
if(true) {
Test x;
}
std::cout << "after\n";
}
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_
isdestroyed
(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
- vector
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
- vector destroys
Resource Acquisition Is Initialization
- object construction: acquire resource
- object destruction: release resource
Example: std::vector
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};
Ownership
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).
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
Common Situation
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.
Problem: Resource Leaks
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.
Solution: RAII Wrapper
- 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 aUsageLog
object UsageLog
can be used to record actions during aDevice
object's lifetime- destructor informs
UsageLog
if aDevice
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
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, …).
Comments…