top of page
Writer's pictureSunil Kumar Yadav

Semantics of C++ Object Destruction

Updated: Dec 14, 2021



C++ is one of the most popular programming languages which is being used in multiple domains. Its diverse use means it supports many programming paradigms. One of the most important features of C++ is the support of Object-Oriented Programming, also known as OOP. Most of the textbooks available in C++ do not go into detail about how C++ objects are being managed internally. This has created many misconceptions about many features of C++. In our last article, we've tried to answer a few misconceptions of new C++ developers, on when compilers synthesize a constructor aka default constructor.


Similarly, new C++ engineers have misconceptions about destructors i.e.

In absence of a user-defined destructor in a class, compiler synthesizes destructor...

When does the compiler synthesize a destructor?

For someone new to the C++ language, the below class Foo (example 1) may give the impression that the compiler will generate a constructor and destructor in absence of a user-provided destructor.
// example 1
class Foo 
{ 
public: 
    int val; 
    int* ptr;
};

void perfrom_action()
{
    int* lptr;
    {// bar object scope start
        Foo bar;
        // program needs bar's members zeroed out
        if ( bar.val || bar.ptr )
           ;     // ... perform some action 
       
        bar.ptr=new int(222); // do something with Foo::ptr
        
        lptr=bar.ptr;     // assign Foo::ptr to local pointer

    }// bar object scope end
 std::cout<<"value inlptr: "<<*lptr<<'\n';  //Foo::ptr is valid on heap
}
If the user has not defined a destructor in a class then the destructor is synthesized only if implementation requires. This means in absence of a destructor in the class, the compiler synthesizes the destructor only if the class contains either member or base class with destructor. In other cases, the destructor is considered to be trivial and therefore neither synthesized nor invoked. This is similar to what we've seen in our earlier article wrt constructor.

Let's try to understand this with the help of a simple example. To understand this we will use the below Point2D class and Line class.
// example 2
class Point2D 
{
public:
  Point( float px = 0.0, float py = 0.0 );  // User defined constructor
  Point( const Point& );                    // Copy constructor
  virtual float z();
  //...
private:
  float x, y;
};
In the above Point2D class we've not defined a destructor and in this case, the compiler will not generate a destructor even though we've virtual function z(). Similarly, when we compose class Line using Point2D, the compiler will not synthesize destructor.
// example 3
class Line 
{
public:
   Line( const Point2D&, const Point2D& );
   //...
   virtual draw();

protected:
  Point2D begin, end;
};
In the above Line class, the compiler will not synthesize destructor as member object of Point2D does not have destructor defined which means there is no need to generate a default constructor by the compiler.

Similarly, if we create class Point3D by inheriting class Point2D by virtual derivation, the compiler will not synthesize the destructor in absence of a user-defined destructor.
// example 4
class Foo 
{ 
public: 
    Foo():val{0},ptr{nullptr} {}
    int val; 
    int* ptr;
    ~Foo()
    {
      if(ptr) delete ptr;
    }
};
If we update the class Foo with a user-defined destructor then the compiler will synthesize the destructor for class Line. The pseudo-code looks something like below. Note, if the Line class holds any resource like heap-allocated memory, the synthesized destructor will not release those resources.
// example 5
// Pseudo code for Line's synthesized destructor
Line::~Line()
{
   begin.Foo::~Foo();
   end.Foo::~Foo();
}


Order of destruction

If we extend our example 2 derive another class called Point3D with a user-defined destructor and class Point3D further extended to class Vertex, which looks something like the below code snippet.


// example 6
class Point3D:: public Point2D
{
  //...
  ~Point3D(); 
}
class Vertex:: public Point3D
{
  //...
}
If we don't provide a destructor for the Vertex class then the compiler will synthesize the destructor for Vectex class, whose only job would be to call Point3D's destructor. In case if we do provide a destructor for Vertex::~Vertex() then the compiler will argument the destructor of Vertex with a call to Point3D's destructor after user-supplied code is executed. User-defined destructor is augmented in the same manner as in the case of constructors, except in the reverse order:
  • If the object contains a virtual pointer i.e. vptr, it is reset to the virtual table associated with the class.

  • The body of the destructor is then executed; i.e. the vptr is reset prior to evaluating the user-supplied code.

  • If the class has member class objects with destructors, these are invoked in the reverse order of their declaration.

  • If there are any immediate nonvirtual base classes with destructors, these are invoked in the reverse order of their declaration.

  • If there are any virtual base classes with destructors and this class represents the most-derived class, these are invoked in the reverse order of their original construction.


Image: Data Layout: Single Inheritance with Virtual Inheritance

Conclusion

From our simple examples we can deduce that for cases like example 2, destructor seems unnecessary, and providing one may be inefficient. To determine if a class needs a program-level destructor (or constructor for that matter), we should consider where the lifetime of a class object terminates (or begin). What needs to be done to maintain invariance? Hence it's very important to follow RAII (Resource Acquisition Is Initialization) idiom. If your object acquires any resource like heap-allocated memory or a file pointer or connection to HTTPS then the user (programmer) should provide pair of constructor and destructor, where constructor will acquire resources and establish invariance of class object and destructor will ensure the release of the resource once a lifetime of an object is over. In cases where runtime polymorphism is involved, the compiler arguments the user-supplied destructor with appropriate calls to reset virtual function pointers which point to the virtual table of the class objects.





Refernces
Inside the C++ Object Model, ISBN: 9780201834543
https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization
https://en.cppreference.com/w/cpp/language/destructor
61 views0 comments

Recent Posts

See All

Comments


bottom of page