Thinh Tran
Thinh Tran

Reputation: 836

Why does whether a struct's destructor runs depend on the type of a member variable?

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?

Upvotes: 25

Views: 1131

Answers (2)

xmh0511
xmh0511

Reputation: 7369

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.

Upvotes: 7

paddy
paddy

Reputation: 63481

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

Upvotes: 16

Related Questions