Search code examples
c++destructor

Why destructor invoked for the 'wrong' object?


I have this code:

class Test
{
public:
    Test(int num)
    {
        this->num = num;
        std::cout << "Test constructed. Num = " << num << std::endl;
    }
    Test(const Test& rhs)
    {
        num = rhs.num;
        std::cout << "Test copy constructed. Num = " << num << std::endl;
    }
    ~Test()
    {
        std::cout << "Test destructed. Num = " << num << std::endl;
    }
    int num;
};


int main()
{
    {
        Test t = Test(1);
        t = Test(2);
    }
}

The code outputs:

Test constructed. Num = 1
Test constructed. Num = 2
Test destructed. Num = 2   <---- I hoped to have here Num = 1
Test destructed. Num = 2

Why is it in the third line of the output is like destructor invoked for the object with value 2 in the 'num' field ('2-num-object' for brevity)? Shouldn't it be invoked for '1-num-object'? We destroy '1-num-object' and instead of it we'll have '2-num-object' in the 't' variable. No?

Trying to comprehend it I even considered that '2-num-object' is on top of the stack, so it should be deleted first, before the '1-num-object', ok. But apart of this internals, after all, it's '1-num-object' destructed, so shouldn't the destructor with the value 1 in the 'num' field be invoked?

Thank you in advance!


Solution

  • Class Test does not declare a copy-assignment operator. The rules are complicated, but in this case, the compiler will supply a default copy-assignment operator. That's because you do declare a destructor and copy-constructor. (See slide 30 of Howard Hinnant's 2014 ACCU presentation.)

    The compiler-supplied, default copy-assignment operator does not call the destructor of class Test. Rather, it performs assignment on each member of class Test in turn. That means assigning member num, which is a simple integer assignment. No destructors are called in the process.

    The destructor for variable t is not called until execution arrives at the closing brace of the block where t is defined.

    The destructor of the temporary object created by Test(2) is called before that. It is called at the end of the expression statement where it appears, at the semi-colon (;) that ends that statement. Thus, the lifetime of the temporary ends at the semi-colon.

    int main()
    {
        {
            std::cout << "Allocating variable `t`...\n\n";
            Test t = Test(1);
            std::cout << "Assignment follows...\n\n";
            t = Test(2);  // <-- destructor for temporary `Test(2)` invoked at semi-colon.
            std::cout << "Assignment complete. Exiting block...\n\n";
        }  // <----------------- destructor for `t` called here.
        std::cout << "Outside block. Exiting function `main`...\n\n";
    }
    

    You can verify this by adding a user-defined copy-assignment operator to class Test. Inside it, check the address of the this pointer, to see which object had its operator= function called. You can also check the address of variable rhs. Similar checks of the this pointer should be made in the constructor and destructor functions.

    With those changes, function main documents the following sequence of events.

    Allocating variable `t`...
    
      Converting Constructor - object: 0, num: 1
    
    Assignment follows...
    
      Converting Constructor - object: 1, num: 2
    
      Inside `operator=`, before assignment:
      rhs                    - object: 1, num: 2
      *this                  - object: 0, num: 1
    
      Still inside `operator=`, after assignment:
      rhs                    - object: 1, num: 2
      *this                  - object: 0, num: 2
    
      Destructor             - object: 1, num: 2
    
    Assignment complete. Exiting block...
    
      Destructor             - object: 0, num: 2
    
    Outside block. Exiting function `main`...
    

    The first two lines show that variable t is allocated as object 0.

    The next two lines show the allocation of object 1, which is the temporary object created by Test(2).

    object 1 is deallocated prior to the message "Assignment complete." object 1, therefore, was destructed at the semi-colon which terminates the assignment.

    object 0 is not destructed until later, after the assignment is complete, and the block is being exited.

    So, object 1, which is the temporary, is deallocated before object 0.

    Details of the program

    Looking at pointer values makes my eyes blur, so I created vector object_id to save them. Every time one of the constructors runs, I push the value of the this pointer onto the back of the vector. The subscript where a pointer is stored becomes its id.

    Given a pointer, function get_id looks it up in the vector, and, if found, returns the subscript where it was found. If not found, function get_id returns -1.

    Inside each of the critical functions, I added get_id(this) to the std::cout statement.

    That gave me the following program.

    // main.cpp
    #include <iostream>
    #include <vector>
    
    class Test;
    using ptr_t = Test const*;
    
    std::vector<ptr_t> object_id;
    
    int get_id(ptr_t const p)
    {
        for (auto id{ object_id.size() }; id--;)
            if (p == object_id[id])
                return static_cast<int>(id);
        return -1;
    }
    
    class Test
    {
        int num;
    public:
        Test(int num) : num{ num }
        {
            object_id.push_back(this);
            std::cout 
                << "  Converting Constructor - object: " << get_id(this) 
                << ", num: " << num << "\n\n";
        }
    
        Test(Test const& rhs) : num{ rhs.num }
        {
            object_id.push_back(this);
            std::cout 
                << "  Copy Constructor       - object: " << get_id(this) 
                << ", num: " << num << "\n\n";
        }
    
        Test& operator= (const Test& rhs)
        {
            std::cout 
                << "  Inside `operator=`, before assignment: \n"
                << "  rhs                    - object: " << get_id(&rhs) 
                << ", num: " << rhs.num << '\n' 
                << "  *this                  - object: " << get_id(this) 
                << ", num: " << num << "\n\n";
    
            this->num = rhs.num;
    
            std::cout 
                << "  Still inside `operator=`, after assignment: \n"
                << "  rhs                    - object: " << get_id(&rhs)
                << ", num: " << rhs.num << '\n'
                << "  *this                  - object: " << get_id(this)
                << ", num: " << num << "\n\n";
            return *this;
        }
    
        ~Test()
        {
            std::cout 
                << "  Destructor             - object: " << get_id(this) 
                << ", num: " << num << "\n\n";
        }
    };
    
    int main()
    {
        {
            std::cout << "Allocating variable `t`...\n\n";
            Test t = Test(1);
            std::cout << "Assignment follows...\n\n";
            t = Test(2);  // <-- destructor for temporary `Test(2)` invoked at semi-colon.
            std::cout << "Assignment complete. Exiting block...\n\n";
        }  // <----------------- destructor for `t` called here.
        std::cout << "Outside block. Exiting function `main`...\n\n";
    }
    // end main.cpp