I'm fairly new to C++ and when wandering around the constructor and destructor behavior I found this problem:
#include <iostream>
struct Student {
std::string a;
~Student() {
std::cout << "Destructor called\n";
}
} S;
int main() {
std::cout << "Before assigning to S\n";
S = {""};
std::cout << "After assigning to S\n";
}
When I compile the code above with g++
and run it, it prints:
Before assigning to S
Destructor called
After assigning to S
Destructor called
But when I change std::string a;
to const char *a;
, then it prints:
Before assigning to S
After assigning to S
Destructor called
Can anyone explain why this change makes the destructor run one fewer time?
Destructor function is called automatically when the object goes out of scope. When a class contains dynamic object then it is mandatory to write a destructor function to release memory before the class instance is destroyed this must be done to avoid memory leak.
The destructor is only one way to destroy the object create by constructor. Hence destructor can-not be overloaded. Destructor neither requires any argument nor returns any value. It is automatically called when object goes out of scope.
The body of an object's destructor is executed, followed by the destructors of the object's data members (in reverse order of their appearance in the class definition), followed by the destructors of the object's base classes (in reverse order of their appearance in the class definition).
A destructor is a member function that is invoked automatically when the object goes out of scope or is explicitly destroyed by a call to delete .
An instance variable or an object is eligible for destruction when it is no longer reachable. A Destructor is unique to its class i.e. there cannot be more than one destructor in a class. A Destructor has no return type and has exactly the same name as the class name (Including the same case).
Whether you need a destructor is NOT determined by whether you use a struct or class. The deciding factor is whether the struct / class has acquired resources that must be released explicitly when the life of the object ends. If the answer to the question is yes, then you need to implement a destructor. Otherwise, you don't need to implement it.
The destructor does not have arguments. It has no return type not even void. An object of a class with a Destructor cannot become a member of the union. A destructor should be declared in the public section of the class.
A Destructor is unique to its class i.e. there cannot be more than one destructor in a class. A Destructor has no return type and has exactly the same name as the class name (Including the same case). It is distinguished apart from a constructor because of the Tilde symbol (~) prefixed to its name.
You are seeing Copy Elision. This is an optimization that will bypass construction of temporary objects in certain cases.
In GCC, that can be disabled by passing the following compiler flag (although it's generally not recommended):
-fno-elide-constructors
In your case, I believe the optimization is triggering due to the struct becoming a trivial type, therefore it's being treated as POD (plain old data) and the memory is simply modified without constructing an object.
Live demo showing the effect of -fno-elide-constructors
: https://godbolt.org/z/c3qrMEr9s
First, in either case, Student
is aggregate. Now, we examine the relevant rule for S = {""};
A braced-init-list may appear on the right-hand side of
- an assignment to an object of class type, in which case the initializer list is passed as the argument to the assignment operator function selected by overload resolution
Since there is no suitable built-in candidate for this case, hence the only viable candidate is the implicitly-declared copy assignment operator, which has the form:
Student& Student::operator=(Student const&)
The corresponding argument is {""}
. Argument passing will initiate copy-initialization, which means the parameter will copy-initialized from {""}
; It's a list-initialization, and the following rule will apply here
Otherwise, if T is a reference type, a prvalue is generated. The prvalue initializes its result object by copy-list-initialization. The prvalue is then used to direct-initialize the reference. The type of the temporary is the type referenced by T, unless T is “reference to array of unknown bound of U”, in which case the type of the temporary is the type of x in the declaration U x[] H, where H is the initializer list.
That means the reference will be bound to the temporary which is copy-initialized by {""}
. That's an aggregate initialization since Student
is an aggregate. And the lifetime of the temporary will be ended at the full-expression, which is ruled by the following rule:
Temporary objects are destroyed as the last step in evaluating the full-expression ([intro.execution]) that (lexically) contains the point where they were created.
Hence, the correct output should be
Before assigning to S
Destructor called // for the temporary
After assigning to S
Destructor called // for the object S
Hence, GCC is wrong here. Since its behavior does not conform to what the standard states. As a contrast, Clang implements the right behavior.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With