1

I am trying to understand what does the C++ standard say about how/when the destructor should be called when an object is returned from the function - Consider this simple struct and two functions -

#include <iostream>
int g = 0;
struct foo {
    int myid;
    foo() {
        myid = g;
        g++;
        std::cout << "Created " << myid << std::endl;
    }
    ~foo() {
        std::cout << "Destroyed " << myid << std::endl;
    }
};

foo bar(void) {
    int i = 0;
    for (foo s; i < 10; i++) {
        if (i == 5)
            return s;
    }
}
foo bar2(void) {
    int i = 0;
    foo s;
    for (; i < 10; i++) {
        if (i == 5)
            return s;
    }
}

int main() {
    bar();
    bar2();
    return 0;
}

I am trying to track how many times the destructor is called. The output from the above program is -

Created 0
Destroyed 0
Destroyed 0
Created 1
Destroyed 1

I can understand the behavior of bar2. An object is created once and destroyed (I believe the destructor is called from main). But in bar when the object is declared inside the loop. It cases the destructor to be called twice. What is the reason for this discrepancy?

Is it the case that the standard leaves this behavior to the implementation (because of copy elision?) and g++ just choses this behavior for the two cases? If so how can I write this function so that I get predictable behavior. I need the destructor to be called the exact same number of times as the constructor (and preferably in the reverse order). I am okay with the destructor being called twice as long as the constructor is being called twice too. The reason is because I am allocating some data inside the constructor and freeing it inside the destructor.

Ajay Brahmakshatriya
  • 8,993
  • 3
  • 26
  • 49
  • 2
    *I need the destructor to be called the exact same number of times as the constructor (* -- Sounds like you need to fix your code so that those side effects you're trying to avoid do not happen. The construction and destruction should work under any and all circumstances. – PaulMcKenzie Apr 26 '20 at 17:08
  • There is not only the default constructor, a compiler-generated move-constructor might be used here. – alain Apr 26 '20 at 17:09
  • you are not instrumenting the copy constructor and move constructor. What are thsoe loops good for? `bar` could simply do `return foo{};` – 463035818_is_not_an_ai Apr 26 '20 at 17:09
  • @idclev463035818 the loops are for demonstration here. They are controlled by some other logic and the value returned depends on when the loop exits. – Ajay Brahmakshatriya Apr 26 '20 at 17:10
  • its not clear what they demonstrate, in the example the loops do nothing, the instance returned is always the same – 463035818_is_not_an_ai Apr 26 '20 at 17:11
  • @alain I also tried instrumenting the copy constructor and realized that it is being called. It still doesn't solve the problem of double free. In the copy constructor should I allocate the resources again so that they don't get double free'd? – Ajay Brahmakshatriya Apr 26 '20 at 17:12
  • @AjayBrahmakshatriya Yes – john Apr 26 '20 at 17:13
  • you need to read about the rule of 3/5, only relying on one constructor to be called when the others are present wont work – 463035818_is_not_an_ai Apr 26 '20 at 17:13
  • 1
    If you have resources, it is probably necessary to implement all the constructors and `operator=`. See rule of 5. – alain Apr 26 '20 at 17:16
  • @AjayBrahmakshatriya You should write your code with no prior knowledge of how many times the constructor and destructor may get called. If you try and write code to depend on the destructor / constructor being called "x" times, that code is wrong. – PaulMcKenzie Apr 26 '20 at 17:19

2 Answers2

2

Add this code

foo(const foo& rhs) {
    myid = g;
    g++;
    std::cout << "Created from copy " << myid << std::endl;
}

This is a copy constructor, it's being called as well only you weren't aware of it, because you were using the default version, which obviously doesn't print anything, or increment your counter.

john
  • 85,011
  • 4
  • 57
  • 81
  • So, I tried this and yes it does print that a copy has been created. This makes sense. But is there a standard compliant way by which I can ensure `bar2` like behavior? I am worried I might be getting the expected behavior from `bar2` because of something implementation defined. – Ajay Brahmakshatriya Apr 26 '20 at 17:17
  • The rules of copy elision have changed a couple of times, first it was allowed, now I think that under some circumstances it is actually required. But what the details are I dont know. You need a proper expert. – john Apr 26 '20 at 17:19
1

cppinsights tells you what's happening: There's a default copy constructor being called, so a copy is being destructed as well.

There, however, both objects are subject to named return value optimisation, a variant of copy elision that elides the copy constructor. If you compile and run your code with clang, that is indeed the case (https://godbolt.org/z/KWhRpL doesn't have the double "Destroyed").

NRVO is optional, and it seems like gcc doesn't apply it there. There is no way to force NRVO to happen, but you could implement a move constructor which will be called instead.

Artyer
  • 31,034
  • 3
  • 47
  • 75