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

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.


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.

Tags:

C++