Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What happens to the old object if you set a variable equal to a new object?

Tags:

c++

Say we have a class X that does not have an overloaded operator=() function.

class X {
    int n;

    X() {n = 0;}
    X(int _n) {n = _n;}
};

int main() {
    X a;    // (1) an object gets constructed here

    // more code...

    a = X(7);    // (2) another object gets constructed here (?)

    // some more code...

    a = X(12);    // (3) yet another object constructed here (?)
    return 0;
}

Is a new object constructed at (2)? If it is, what happens to the old object that was constructed at (1)? Is it automatically destructed or deallocated (which is it)? Is it overwritten?

And what happens farther down the code at (3)?

And most importantly, is there ever a chance of causing a memory leak by writing code like in the above?

like image 942
Manuel Avatar asked Jun 22 '17 18:06

Manuel


People also ask

When changing the value of a variable to a new value what happens to the old value?

A variable can only hold one value, so if you assign a new value to a variable the old value is gone.

What happens when you assign an object to another object?

what happens when you assign one object to another object ?? It assigns the reference of the Object. In other words, both variables 'point' to the same Object. For example/ int [] a = new int[5]; int [] b = a; b and a now 'point' to the same array.

What happens when you assign the value of one variable to another variable in Python?

Assigning one variable to another variable creates an alias of each variable. An alias is variable that points to the same object in memory as another variable. In the example above, both variables var1 and var2 are aliases of each other. In Python, it is possible to destroy references.

Does the data type of a variable change when the value inside the variable changes?

Short answer: no.


2 Answers

What you need to understand is that there's a lot of "implicit" code generated by the compiler that you, as a novice, don't know about. We'll use your code for class X as a direct example:

class X {
    int n;
public: //You didn't include this, but this won't work at all unless your constructors are public
    X() {n = 0;}
    X(int _n) {n = _n;}
};

Before the code gets turned into Object Code, but after your compiler gets ahold of your class definition, it transforms your class into something that looks (roughly) like this:

class X {
    int n;
public:
    X() {n = 0;} //Default-Constructor
    X(int _n) {n = _n;} //Other Constructor
    //GENERATED BY COMPILER
    X(X const& x) {n = x.n;} //Copy-Constructor
    X(X && x) {n = x.n;} //Move-Constructor
    X & operator=(X const& x) {n = x.n; return *this;} //Copy-Assignment
    X & operator=(X && x) {n = x.n; return *this;} //Move-Assignment
    ~X() noexcept {} //Destructor
};

The rules for when these members are automatically created are not super-obvious (A good starting reference here), but for now, you can trust that in this case, that's exactly what happens.

So in your main function, let's go over what happens, with the specifics called attention to with comments:

int main() {
    X a; //Default-Constructor called
    a = X(7);//Other Constructor called, then Move-Assignment operator called,
    //then Destructor called on temporary created by `X(7)`
    a = X(12); //Same as previous line

    return 0;
    //Destructor called on `a`
}

We'll add a few more lines to show most (if not all) of the various permutations of these calls:

int main() {
    X a; //Default-Constructor
    X b = a; //Copy-Constructor (uses copy-elision to avoid calling Default + copy-assign)
    X c(5); //Other Constructor
    X d{7}; //Also Other Constructor
    X e(); //Declares a function! Probably not what you intended!
    X f{}; //Default-Constructor
    X g = X(8); //Other Constructor (uses copy-elision to avoid calling Other + move-assign + Destructor)
    X h = std::move(b); //Move-Constructor (uses copy-elision to avoid calling Default + move-assign)
    b = c; //Copy-assignment
    b = std::move(d); //Move-assignment
    d = X{15}; //Other Constructor, then Move-Assignment, then Destructor on `X{15}`.
    //e = f; //Will not compile because `e` is a function declaration!
    return 0;
    //Destructor on `h`
    //Destructor on `g`
    //Destructor on `f`
    //Destructor will NOT be called on `e` because `e` was a function declaration, 
    //not an object, and thus has nothing to clean up!
    //Destructor on `d`
    //Destructor on `c`
    //Destructor on `b`
    //Destructor on `a`
}

That should cover the basics.

And most importantly, is there ever a chance of causing a memory leak by writing code like in the above?

As written, no. However, suppose your class did something like this instead:

class X {
    int * ptr;
public:
    X() {
        ptr = new int{0};
    }
};

Now, your code would leak, because every time an X was created, you'd have a pointer that never gets deleted.

To solve this, you need to make sure that A) The destructor properly cleans up the pointer, and B) that your copy/move constructors/operators are correct.

class X {
    int * ptr;
public:
    X() {
        ptr = new int{0};
    }
    X(int val) {
        ptr = new int{val};
    }
    X(X const& x) : X() {
        *ptr = *(x.ptr);
    }
    X(X && x) : X() {
        std::swap(ptr, x.ptr);
    }
    X & operator=(X const& x) {
        *ptr = *(x.ptr);
        return *this;
    }
    X & operator=(X && x) {
        std::swap(ptr, x.ptr);
        return *this;
    }
    ~X() noexcept {
        delete ptr;
    }
};

This code will not leak memory if used as-is in either your main function or mine. But of course, it doesn't stop the leaks if you do something like this:

int main() {
    X * ptr = new X{};
    return 0;
    //Whelp.
}

In general, if you never need to use pointers at all, it's recommended you use something like std::unique_ptr instead, as it gives most of this stuff for free.

int main() {
    std::unique_ptr<X> ptr{new X{}};
    return 0;
    //Destructor called on *ptr
    //`delete` called on ptr
}

And it's a good idea in your original class, with a caveat that, unless you explicitly change it, your class won't by copyable anymore (though it'll still be movable):

class X {
    std::unique_ptr<int> ptr;
public:
    X() {
        ptr.reset(new int{0});
    }
    X(int val) {
        ptr.reset(new int{val});
    }
    //X(X && x); //auto generated by compiler
    //X & operator=(X && x); //auto generated by compiler
    //~X() noexcept; //auto generated by compiler

    //X(X const& x); //Deleted by compiler
    //X & operator=(X const& x); //Deleted by compiler
};

We can see the changes in my previous version of main:

int main() {
    X a; //Default-Constructor
    //X b = a; //Was Copy-Constructor, no longer compiles
    X c(5); //Other Constructor
    X d{7}; //Also Other Constructor
    X f{}; //Default-Constructor
    X g = X(8); //Other Constructor (uses copy-elision to avoid calling Other + move-assign + Destructor)
    X h = std::move(c); //Move-Constructor (uses copy-elision to avoid calling Default + move-assign)
    //b = c; //Was Copy-assignment, no longer compiles
    c = std::move(d); //Move-assignment
    d = X{15}; //Other Constructor, then Move-Assignment, then Destructor on `X{15}`.
    return 0;
    //Destructor on `h`
    //Destructor on `g`
    //Destructor on `f`
    //Destructor on `d`
    //Destructor on `c`
    //Destructor on `a`
}

If you want to use std::unique_ptr, but also want the resulting class to be copyable, you'll need to implement the copy constructor yourself using the techniques I discussed.

And that should be about it! Let me know if I missed anything.

like image 79
Xirema Avatar answered Oct 17 '22 06:10

Xirema


At point (2) three things happen:

  1. A temp object is constructed using the X(int _n) constructor.
  2. The default assignment operator is used to copy the contents of the temporary to a.
  3. The temporary goes out of scope and its default destructor is invoked.

The same thing happens at point (3).

At the end of the function, the default destructor on a is invoked.

like image 41
dbush Avatar answered Oct 17 '22 07:10

dbush