2

Problem

The destructor gets called twice in the following code:

class Foo
{
public: 
    ~Foo()
    {
        std::cout << "Destructor called\n";
    }

    Foo& operator=(const Foo& other)
    {
        std::cout << "Assignment called\n";

        return *this;
    }
};

Foo foo()
{
    return Foo();
}

int main()
{
    foo();

    return 0;
}

Output:

Destructor called
Destructor called

I suspect this is due to some implicit call to an assignment operator or a copy constructor. I cannot tell if the copy constructor gets called as adding a constructor of any kind magically solves the problem (as explained further down), but at least the assignment operator does not get called.

And as mentioned, if I add a constructor, the problem goes away:

class Foo
{
public:
    Foo()
    {
        std::cout << "Constructor called\n";
    }

    Foo(const Foo& other)
    {
        std::cout << "Copy constructor called\n";
    }

    ~Foo()
    {
        std::cout << "Destructor called\n";
    }

    Foo& operator=(const Foo& other)
    {
        std::cout << "Assignment called\n";

        return *this;
    }
};

The output changes into:

Constructor called
Destructor called

The problem also goes away if I return a reference instead of an object (but results in a "returning address of local or temporary variable" warning):

Foo& foo()
{
    return Foo();
}

Question

Why is the destructor called twice, and why is the behavior different when using a default constructor or not? Is there a logical explanation or could it be a mistake by the compiler?

I'm using MSVC 2013 if it could make any difference.

Columbo
  • 60,038
  • 8
  • 155
  • 203
Adelost
  • 2,343
  • 2
  • 23
  • 28
  • 2
    possible duplicate of [What are copy elision and return value optimization?](http://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization) – P0W Oct 04 '14 at 13:06
  • 1
    You forgot to include the copy constructor. And to test the code in the Release build so that the Return Value Optimization can avoid the extra copy. – Hans Passant Oct 04 '14 at 13:07
  • 2
    Assignment operator should return non-const reference. – Neil Kirk Oct 04 '14 at 13:07
  • @HansPassant As far as I know, ordinary (non-named) RVO should already be applied without optimizations. – dyp Oct 04 '14 at 13:10
  • Just to back up @NeilKirk's observation, even in C++03 the "assignable" requirement on standard container items was that a copy assignment operator of the form `T& operator=( T const& )`. – Cheers and hth. - Alf Oct 04 '14 at 13:10
  • Cannot reproduce that: http://coliru.stacked-crooked.com/a/c3110215b2ccada2 (clang++-3.5.0 g++-4.9.0 / c++03 c++11 c++14 / -O3) – Deduplicator Oct 04 '14 at 13:10
  • @Deduplicator http://rextester.com/CNHO85187 – dyp Oct 04 '14 at 13:11
  • 1
    The number of destructor calls depends on the number of constructor calls, which depends on the optimizations, i.e. the compiler and options. – Cheers and hth. - Alf Oct 04 '14 at 13:11
  • @Cheersandhth.-Alf Strangely enough, once you add a default ctor with side effects, the dtor is only called once: http://rextester.com/YOBV7207 – dyp Oct 04 '14 at 13:12
  • @NeilKirk Thanks for pointing out the const assignment operator, the problem still persists though. – Adelost Oct 04 '14 at 13:14
  • 1
    How do you know that no copy constructor is called in the first example? – milleniumbug Oct 04 '14 at 13:21
  • @HansPassant I could not include a copy constructor in the first example as the compiler would replace the default constructor with the copy constructor, and including a constructor of any kind magically solves the problem. I tried the code in Release as well, but as I mentioned only in Visual Studio. – Adelost Oct 04 '14 at 13:23
  • @milleniumbug I do not, perhaps I should add that to be more specific. But including a a constructor of any kind magically solves the problem. – Adelost Oct 04 '14 at 13:25
  • 1
    replace `std::cout << "Destructor called\n";` with `std::cout << "Destructor called on this=" << this << '\n';` also add output in the code reporting the address of any `Foo` objects. Then you'll see which objects are destroyed. – Walter Oct 04 '14 at 13:30
  • @Walter That is clever. It shows that it is indeed different objects in the first example, and the same objects in the last. Although, we already suspected as much. – Adelost Oct 04 '14 at 13:43
  • 1
    @P0W Thanks, the link pretty much answered my question. I now know that copy elision can be applied even if copying/moving the object has side-effects, and that it can be different from compiler to compiler. – Adelost Oct 04 '14 at 14:45

3 Answers3

1

The return statement copies your return value into the temporary object in scope of the calling function, that is, main. Then the temporary object Foo(), created in scope of foo, gets destructed, and then the temporary in scope of main gets destructed. The compiler is allowed, but not obligated, to optimize this away.

Armen Tsirunyan
  • 130,161
  • 59
  • 324
  • 434
1
Foo foo()
{
    return Foo(); // temporary is created and copied into the return value. 
                  // The temporary gets destroyed at the end of the copy constructor.
}

int main()
{
    foo(); // discarded-value expression; the return value gets destroyed immediately.
}

The compiler is allowed to apply copy-elision - in this case, NRVO (§12.8/31):

— when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move

Note that this works regardless of whether the copy constructor has any side effects or not. This is an "optimization" that can legally change the observable behavior of the program without disobeying the standard.

Since the compiler is not obliged to optimize anything, results may differ from code to code (and optimization level).

Columbo
  • 60,038
  • 8
  • 155
  • 203
  • I liked your answer the best. But I would also like to point out the important fact that copy elision can be applied even if copying/moving the object has side-effects http://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization (cred to @P0W). – Adelost Oct 04 '14 at 14:59
  • @Adelost I know, i thought that it was obvious through the quote. Should i point it out in the answer? – Columbo Oct 04 '14 at 15:06
  • I think so. That one confused me a lot since I did not think the compiler was allowed to make optimizations when the optimization has side effects, and it is sometimes better to be explicit. – Adelost Oct 04 '14 at 15:17
-1

In the function foo() your object gets created. It returns a copy of that object. When the function ends, the first object (created in the function) goes out of scope, invoking the destructor.

Next, you main() gets a whole new object. And when it ends, it invokes destructor of this new object.

For the following:

Foo& foo()
{
   return Foo();
}

..You return reference to a local variable which gives a worthless return and main function doesn't get an object at all, no duplicate destructor is called.

Noobification
  • 319
  • 1
  • 14