Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++: Destructor being called outside object scope?

Tags:

c++

destructor

Update 1: Added printing "this" as suggested.

Update 2: Split into several files to try and stop gcc from optimizing.

Update 3: Logged copy constructor and entering add functions.

Update 4: Added output from Clang and second cout in main.

I expect parameter destructors to be called as the last statements in a function. Henceforth, I would expect the following output from the code below.

default constructor: 008DFCF8
other constructor: 008DFCEC
copy constructor: 008DFBC0
in member add
destroying: 008DFBC0
copy constructor: 008DFBB8
copy constructor: 008DFBB4
in function add
destroying: 008DFBB4
destroying: 008DFBB8
3 == 3
end of main
destroying: 008DFCEC
destroying: 008DFCF8

When using MSVC (Visual Studio), the output is as expected. But GCC (4.8.2-19ubuntu1) outputs the following that shows that the destructors for the function parameters are being called after the first cout statement in main() but before the last one.

default constructor: 0x7fff2fcea510
other constructor: 0x7fff2fcea520
copy constructor: 0x7fff2fcea550
in member add
copy constructor: 0x7fff2fcea540
copy constructor: 0x7fff2fcea530
in function add
3 == 3
destroying: 0x7fff2fcea530
destroying: 0x7fff2fcea540
destroying: 0x7fff2fcea550
end of main
destroying: 0x7fff2fcea520
destroying: 0x7fff2fcea510

For those that are curious what clang++ (3.4-1ubuntu3) outputs.

default constructor: 0x7fff52cf9878
other constructor: 0x7fff52cf9870
copy constructor: 0x7fff52cf9860
copy constructor: 0x7fff52cf9858
in function add
3 == copy constructor: 0x7fff52cf9850
in member add
3
destroying: 0x7fff52cf9850
destroying: 0x7fff52cf9858
destroying: 0x7fff52cf9860
end of main
destroying: 0x7fff52cf9870
destroying: 0x7fff52cf9878

Questions:

  1. My initial suspicion is that GCC is inlining the functions? If this is true is there a way to disable this optimization?
  2. What section in the C++ spec would allow the destructors to be called after the cout in main? Especially of interest are the rules on inlining, if relevant, and when destructors are scheduled.

// Test.h
#ifndef __TEST_H__

#include <iostream>

using namespace std;

class Test
{
public:
    int val;

    Test(Test const &a) : val(a.val)
    {
        cout << "copy constructor: " << this << endl;
    }

    Test() : val(1)
    {
        cout << "default constructor: " << this << endl;
    }

    Test(int val) : val(val)
    {
        cout << "other constructor: " << this << endl;
    }

    ~Test()
    {
        cout << "destroying: " << this << endl;
    }

    int add(Test b);
};

#endif

// Add.cpp
#include "Test.h"

int Test::add(Test b)
{
    cout << "in member add" << endl;
    return val + b.val;
}

int add(Test a, Test b)
{
    cout << "in function add" << endl;
    return a.val + b.val;
}

// Main.cpp
#include "Test.h"

int add(Test a, Test b);

int main()
{
    Test one, two(2);

    cout << add(one, two) << " == " << one.add(two) << endl;

    cout << "end of main" << endl;

    return 0;
}

Compiled for GCC using:

g++ -c Add.cpp -o Add.o ; g++ -c Main.cpp -o Main.o ; g++ Add.o Main.o -o test
like image 872
Justin Avatar asked Oct 08 '14 05:10

Justin


3 Answers

It seems that the C++ standard might be a little ambiguous regarding exactly when function parameter destructors must be called. C++03 and C++11 both say in 5.2.2/4 "Function call" (emphasis added):

The lifetime of a parameter ends when the function in which it is defined returns. The initialization and destruction of each parameter occurs within the context of the calling function.

So the destructor for parameters doesn't occur conceptually at the function's closing brace. This is something I didn't know.

The standard gives a note explaining how this means that if the destructor for a parameter throws, then only exception handlers for the calling function or 'higher' are considered (specifically, even if the called function has a 'function-try-block', it is not considered).

While I think that the intent is for the MSVC behavior, I could see how someone might interpret a reading that allows the GCC behavior.

Then again, maybe it's a bug in GCC?

like image 200
Michael Burr Avatar answered Sep 20 '22 18:09

Michael Burr


You create temporary objects when calling "add(a,b)", and when calling the member add(b). I think what you are seeing in the case of gcc is that the local variables in the add() functions (the parameters) are destroyed when those functions return. The last two "done" lines are for the variables "one" and "two".

VC is different - but this isn't wrong, it just shows that the two compilers are optimizing the code in different ways.

Instead of just printing "done", try also printing the value "this". Print "this" in the constructor as well, then you can see how the constructors and destructor calls are paired.

Oops - I mixed that up a bit between VC and GCC. VC prints "done" three times first - presumably as the add() parameters are destroyed, while GCC does them all last, probably because it inlined the add functions.

like image 35
joeking Avatar answered Sep 19 '22 18:09

joeking


Consider this line:

cout << add(one, two) << " == " << one.add(two) << endl;

Written as:

cout << add(one, two);
cout << " == " << one.add(two) << endl;

Would that change the printout for GCC?

Or that way:

auto i = add(one, two);
cout << i << " == ";
auto j = one.add(two)
cout << j << endl;

I think that this is about side-effects (not about inlining). VC seems to be scheduling side-effects (destruction of temporary objects) earlier, while GCC schedules it at end of statement - ;


Added quote:

Temporary object lifetime

Temporary objects are created in various situations: binding a reference to a prvalue, returning a prvalue from a function, cast to a prvalue, throwing an exception, entering an exception handler, and in some initializations. In every case, all temporaries are destroyed as the last step in evaluating the full-expression that (lexically) contains the point where they were created, and if multiple temporaries were created, they are destroyed in the order opposite to the order of creation. This is true even if that evaluation ends in throwing an exception.

In my opinion, this speaks for GCC and against VC (especially about printing "3 == 3" after the destruction, that is strange for me).

like image 35
firda Avatar answered Sep 16 '22 18:09

firda