Reputation: 187
I am new to C++ and I wrote a small program to learn about how assignment works with objects. I was prompted to do this from the cpp docs at this page (http://www.cplusplus.com/doc/tutorial/classes2/). On this page it states:
The implicit version [of the copy assignment operator] performs a shallow copy which is suitable for many classes, but not for classes with pointers to objects they handle its storage. In this case, not only the class incurs the risk of deleting the pointed object twice, but the assignment creates memory leaks by not deleting the object pointed by the object before the assignment.
The final part which I have formatted in bold is why I had decided to test things out. I thought that this problem could be solved by handling the deletion of pointed objects in the destructor (which is standard?), rather than having to overload the copy assignment operator. If the destructor is not being called, then isn't that really inconvenient? Say I had multiple referenced objects, I would have to put all the deletes in both the destructor (for most cases of reallocation) and also in the assignment overload.
During this test, I encountered an entirely different problem. My initial idea was to create a simple class which stores an int (as an identifier for testing purposes) and overload the constructors and destructors to see when and if the destructor was being called.
Here is my code:
class Test{
public:
int id;
explicit Test(int id) : id(id) {
cout << "Created " << id << endl;
}
~Test() {
cout << "Destroyed " << id << endl;
}
};
int main() {
Test x = Test(1);
x = Test(2);
cout << x.id << endl;
return 0;
}
The output I would have expected was to be:
1: Created 1
2: Destroyed 1
? (this was the one I was unsure of as the website hinted this destructor would not be called if the object was 'replaced' with another rather than going out of scope)
3: Created 2
Object 2 'replaces' object 1 as it is assigned to x
4: 2
the value of Object 2's id printed out
5: Destroyed 2
Object 2 destroyed as it goes out of scope
Instead, i got the following output:
Created 1
Created 2
Destroyed 2
2
Destroyed 2
This really does not make sense to me.
Using the debugger, Created 2
and Destroyed 2
both display when the line x = Test(2);
is called. If we just assigned x
to Object 2, why is its destructor being called immediately? This follows on to the next part.
Secondly, since the destructor for Object 2 has been called, we can assume it has been destroyed. The next output of 2
seems to contradict this, as it suggests that x
is still holding Object 2 (expected, but contradicted by the call of its destructor).
I'm not too sure why this is happening.
Finally, Destroyed 2
is outputted. This would make sense if we didn't see this earlier. Object 2 is stored in x
, so when it goes out of scope the destructor is called.
For some reason we get the destructor called twice, and Object 1, which is 'overridden' by the assignment of Object 2 to x
never has its destructor called, but instead the destructor of the object we just created has its destructor called.
So... this sums up to a two-part question:
1: Why is this weird behaviour occurring, and is there any logical reason to why it is?
2: Does 'overwriting' an object (e.g. Object 1) with another object (Object 2) by assignment lead to its destructor (in this case the destructor of Object 1) to be called or not?
Thanks in advance.
Upvotes: 0
Views: 421
Reputation: 25593
Test x = Test(1);
This creates a new object with the value of "1".
x = Test(2);
This first creates a new object with value of "2" and after that it will be assigned to the first object with assignment operator which is implicitly created for your class! In this moment, you have two objects, both having the value of 2!
To get a better idea you can do this:
class Test{
public:
static int instanceCount;
int id;
int count;
explicit Test(int id) : id{id}, count{instanceCount++} {
std::cout << "Created " << id << " " << count << std::endl;
}
~Test() {
std::cout << "Destroyed " << id << " " << count << std::endl;
}
//Test& operator=(const Test&) = delete;
Test& operator=(const Test& ex)
{
id=ex.id;
return *this;
}
};
int Test::instanceCount = 0;
int main() {
Test x = Test{1};
x = Test{2};
std::cout << x.id << std::endl;
return 0;
}
Now you can see when a new instance is created. If you delete the assignment operator for your class, you will see that the first instruction you wrote "Test x = Test{1};" is NOT an assignment but construction. The second one "x = Test{2};" will fail, as you have deleted the operator now.
The output is as follows:
Created 1 0
Created 2 1
Destroyed 2 1
2
Destroyed 2 0
As you see, you get first a instance with count 0 and your value 1. The second temporary instance is then created as count 1 with your value of 2. Then this one will be assigned to the first one and the temporary instance will be deleted before your std::cout will happen! In the moment you left the main function scope, the first instance will be deleted!
What you can learn:
X x=X(3);
is the same as writing X x(3);
using namespace std
!X x{3} instead of
X x(3)`X x=X(3);
is totally confusing, as it looks like you construct a temporary and than assign it to a default constructed one. But this will not happen and so you should write your code simpler instead!Upvotes: 2
Reputation: 29023
Using the debugger, Created 2 and Destroyed 2 both display when the line x = Test(2); is called. If we just assigned x to Object 2, why is its destructor being called immediately? This follows on to the next part.
The line x = Test(2);
first create a Test
with the constructor argument 2
. This is what produces Created 2
. This nameless Test
is then assigned to x
which gives x.id
the value 2. That nameless Test
is then destroyed at the end of the expression, producing "Destroyed 2".
Secondly, since the destructor for Object 2 has been called, we can assume it has been destroyed. The next output of 2 seems to contradict this, as it suggests that x is still holding Object 2 (expected, but contradicted by the call of its destructor).
As expressed in the first part of this answer, it's not x
that is destroyed but the temporary Temp
. x.id
is still valid and will yield it's new value, 2.
Finally, Destroyed 2 is outputted. This would make sense if we didn't see this earlier. Object 2 is stored in x, so when it goes out of scope the destructor is called.
This happens when x
is destroyed at the end of the function. It's id
value was changed to 2 by the previous assignment, so it produces "Destroyed 2".
1: Why is this weird behaviour occurring, and is there any logical reason to why it is?
It may not be the behavior you expected, but it's not weird. I hope this answer will help you understand why it's occurring.
2: Does 'overriding' an object (e.g. Object 1) with another object (Object 2) by assignment lead to its destructor (in this case the destructor of Object 1) to be called or not?
Assigning to an object does not destroy it. It replaces it's value with a new one and in that sense "destroys" the value it previously help but the actual object instance is not destroyed and the destructor is not involved.
Edit : It seems you may be concerned with resource leaks. Since no resources are being managed by Test
there will be no leaks and the compiler generated members will behave fine. If your class does manage resources (usually in the form of dynamically allocated memory) then you will need to apply the rule of 3/5/0. Notably you will need to implement the assignment operator yourself such that it cleans up any previously held resources. It's not enough to implement just the destructor as it's not involved in assignment.
Upvotes: 4