3

I saw this question When is an object "out of scope"?

I have taken a look at the sparc_spread's answer and I found one problem in it. In this section of his answer:

Circle myFunc () {
    Circle c (20);
    return c;
}
// The original c went out of scope. 
// But, the object was copied back to another 
// scope (the previous stack frame) as a return value.
// No destructor was called.

He has said that " No destructor was called. " But when I try to run this code ( Which was written by me):

   /* Line number 1 */ #include <iostream>
   /* Line number 2 */ #include <string>
   /* Line number 3 */ using namespace std;
   /* Line number 4 */ class test {
   /* Line number 5 */ public:
   /* Line number 6 */  test(int p) {
   /* Line number 7 */      cout << "The constructor ( test(int p) ) was called"<<endl;
   /* Line number 8 */  }
   /* Line number 9 */  test(test&&c)noexcept  {
   /* Line number 10 */        cout << "The constructor ( test(test && c) ) was called" << endl;
   /* Line number 11 */ }
   /* Line number 12 */     ~test() {
   /* Line number 13 */         cout << "The distructor was called" << endl;
   /* Line number 14 */     }
   /* Line number 15 */ };
   /* Line number 16 */ test function() {
   /* Line number 17 */     test i(8);
   /* Line number 18 */     return i;
   /* Line number 19 */ } 
   /* Line number 20 */ int main()
   /* Line number 21 */ {
   /* Line number 22 */     test o=function();
   /* Line number 23 */     return 0;
   /* Line number 24 */ }

The Output:

The constructor ( test(int p) ) was called
The constructor ( test(test && c) ) was called
The distructor was called
The distructor was called

So the output of my code shows that:

  1. Two constructors were called ( and this is not the point I want to discuss. So I will not discuss ( Why, When or How) are two constructors called?)

  2. Two destructors were called

And when I use the debugger (to know when the first destructor was called) I found that The first destructor is called in line number 18 (the line number 18 in my code).

And in the end. Is my point of view right?

Jason
  • 36,170
  • 5
  • 26
  • 60
f877576
  • 447
  • 2
  • 7
  • First constructor: `test i(8);` Second constructor: When the returned value is moved into the `o` of the `main` function (the `this` object will be a pointer to `o`). First destruction: When the life-time of `i` ends. Second destruction: When the life-time of `o` end. – Some programmer dude May 25 '22 at 19:26
  • 6
    I get no move constructor and only one destructor call, due to [NRVO](https://en.cppreference.com/w/cpp/language/copy_elision). https://godbolt.org/z/v8Kxvo79c – Fred Larson May 25 '22 at 19:31
  • @Some programmer dude So the destructor will be called for i ( in the code which was written by me ) . and the second constructor for o ( in the main function ) and that is why The constructor ( test(test && c) is the second constructor ( because the returned value is an rvalue ) , right? – f877576 May 25 '22 at 19:38
  • The compiler is allowed to optimize aggressively here and can eliminate the construction, copying and destruction of `i`. This is one of the few places in the language where an observable behaviour like the diagnostic messages your code prints, can be omitted. Depending on how "smart" or aggressive the compiler is in seeking out optimizations you may or may not see the life and death of `i`. So sparc_spread's answer may or may not be correct for you depending on your tools and optimizations requested. – user4581301 May 25 '22 at 19:42
  • Isn’t this more of a meta question? – Taekahn May 25 '22 at 19:43
  • I've been deliberately ignoring the bits asking for (I think) changes to the source answer and looking at the question as more a request for clarification. – user4581301 May 25 '22 at 19:47
  • Question, @f877576 : What compiler and compiler options are you using to build this code? I'm having trouble finding a compiler that will accept C++11 and won't perform this optimization without adding an option that rejects eliding. – user4581301 May 25 '22 at 19:51
  • @Fred Larson [link](https://en.cppreference.com/w/cpp/language/copy_elision) **copy_elision** is for constructors how does your code in the link does not appear the second destructor – f877576 May 25 '22 at 19:52
  • @f877576 I think you're reading something incorrectly there. Can you reproduce the exact quote or link us directly to it so that we can confirm or correct your interpretation? – user4581301 May 25 '22 at 19:54
  • @f877576: If no copy is made, and therefore no constructor invoked, no destructor call will be made. – Fred Larson May 25 '22 at 19:55
  • @user4581301 I use Microsoft Visual Studio Community 2022 – f877576 May 25 '22 at 19:55
  • @user4581301 go with msvc with no optimization, it's always a safe bet – Federico May 25 '22 at 19:56
  • Huh. Can't believe I didn't try that one. OK. Microsoft is passing up on the opportunity. Are you making a release or debug build? Looks like a debug build. The optimization is made when built with /O2. – user4581301 May 25 '22 at 19:56
  • @Fred Larson okay i understand .but is there any way to add the flag that disable the copy_ elision in the compiler you use in the link – f877576 May 25 '22 at 20:00
  • 2
    A note on Debug builds. They're deliberately stupid. In order to represent the code as written to make it easy to debug, they generally perform NO optimization. GCC and clang seem to optimize this with or without optimization unless you demand no elision. Here's Fred's link with `-fno-elide-constructors`: https://godbolt.org/z/vvrefajz9 – user4581301 May 25 '22 at 20:02
  • Don't put too much weight on debug builds of code, though. You rarely want to ship debug executables. They are exceptionally slow and may include information you don't want in the hands of folks who seek to abuse the program. – user4581301 May 25 '22 at 20:04
  • @user4581301 I have 2 questions do you mean the options release or debug build in this photo [link](https://ibb.co/8gsQQ4m) ? and if your answer is yes i try to compile and run another code (using debug built ) and the copy _ elision works how does this happen ? – f877576 May 25 '22 at 20:13
  • Yes. Change the Debug UI item to Release and you should see what we've been seeing, baring other surprises. We're quibbling over an **optional** optimization, almost universally supported, but still optional. There are many elision strategies. A common one is for the compiler to silently transform `test function()` into something along the lines of `void function(test & i)` and `test o=function();` into `test o; function(o);`. – user4581301 May 25 '22 at 20:22
  • In general, [the compiler is allowed to do anything it wants to your code so long as it doesn't change the observable behaviour](https://en.cppreference.com/w/cpp/language/as_if) , but elision is special and is allowed to change the observable behaviour in order to eliminate a copy. It is important to think of code not as a list of instructions for the computer to run, but as a description of the behaviour you want the program to have. The compiler's job is then to take the behaviour described by the code and produce the optimal list of instructions. – user4581301 May 25 '22 at 20:24
  • @user4581301 Okay but How the elision sometimes works in the debug build and sometimes does not work ? – f877576 May 25 '22 at 20:27
  • It's an optional optimization. GCC made the choice to keep the unoptimized program just that little bit closer to the optimized version, unless you specifically ask for it to not happen, and sometimes that's helpful when debugging. Microsoft took the other approach and probably had good, but different, reasons for doing it. – user4581301 May 25 '22 at 20:30
  • @user4581301 So for the last once the destructor will not be called for `i` because of copy_elision and if you disable copy_elision the distructor will be called ? – f877576 May 25 '22 at 20:35
  • With elision supported, the destructor will not be called for `i` because there is no `i`. `i` was not constructed and doesn't need to be destroyed. Disable elision and `i` will have to be created, copied or moved, and destroyed. – user4581301 May 25 '22 at 20:38
  • @user4581301 I am sorry for bothering you but here and with elision supported the constructor is called [link](https://wandbox.org/permlink/gkwg3YWDRHlzsAoB) – f877576 May 25 '22 at 20:55
  • Different case. In that one `i` is not returned. Without a copy to elide, there can be no copy elision. – user4581301 May 25 '22 at 21:21
  • [link](https://wandbox.org/permlink/IGxuP1U78fjZEzXZ) @user4581301 I changed it and make that : `i` is returned and with elision supported and the constructor for `i` is called . how does this work ? – f877576 May 25 '22 at 21:30
  • There are some fairly particular requirements to get copy elision. [Read through this](https://en.cppreference.com/w/cpp/language/copy_elision) and make sure you met them all. If you have any questions, they'll probably be different enough from what you asked here to be worth asking a new question focused on your current experiments. – user4581301 May 25 '22 at 21:37
  • @ user4581301 I want to know in this comment which was written by you " Huh. Can't believe I didn't try that one. OK. Microsoft is passing up on the opportunity. Are you making a release or debug build? Looks like a debug build. The optimization is made when built with /O2. " What is the meaning of " /O2 " ? – f877576 May 25 '22 at 23:53
  • `/O2` on the command line tells visual Studio's compiler `cl` to [optimize the code for maximum speed](https://learn.microsoft.com/en-us/cpp/build/reference/o1-o2-minimize-size-maximize-speed?view=msvc-170). – user4581301 May 26 '22 at 00:59

1 Answers1

2

Does returning a local variable return a copy and destroy the original?

The final answer to your question is that it depends on whether or not optimization is enabled. So lets discuss each case separately. Note also that since the given output in the original question is for C++17, the below discussion is also for the same(C++17 & onwards).

With Optimization

Here we will see what happens when optimization(NRVO) is enabled.

class test {
public:
test(int p) {
    cout << "The constructor ( test(int p) ) was called: "<<this<<endl;
}
test(test&&c)noexcept  {
       cout << "The constructor ( test(test && c) ) was called: "<<this << endl;
}
    ~test() {
        cout << "The distructor was called: "<<this << endl;
    }
};
test function() {
    test i(8);
    return i;
} 
int main()
{
    test o=function();
    return 0;
}

The output of the program is(with NRVO enabled):

The constructor ( test(int p) ) was called: 0x7fff78e42887   <-----object o construction
The distructor was called: 0x7fff78e42887                    <-----object o destruction

The above output can be understood using the optimization called named return value optimization(aka NRVO) as described in copy elison which states:

Under the following circumstances, the compilers are permitted, but not required to omit the copy and move (since C++11) construction of class objects even if the copy/move (since C++11) constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. This is an optimization: even when it takes place and the copy/move (since C++11) constructor is not called, it still must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed:

  • In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a function parameter or a catch clause parameter, and which is of the same class type (ignoring cv-qualification) as the function return type. This variant of copy elision is known as NRVO, "named return value optimization".

(emphasis mine)

Lets apply this to our example given above and try to understand the output. The variable named i is a local variable meaning it has automatic storage duration and thus according to the above quoted statement, the compilers are allowed(but not required!) to directly construct the object into the storage for variable named o. That is, it is as if you wrote:

test o(5); //equivalent to this due to NRVO

Thus here we first see the call to the converting constructor test::test(int) for object o and then the destructor call for that object o.

Without Optimization

You have the option to disable this optimization by using the -fno-elide-constructors flag. And when executing the same program with this flag, the output of the program will become:

The constructor ( test(int p) ) was called: 0x7ffda9d94fe7        <-----object i construction
The constructor ( test(test && c) ) was called: 0x7ffda9d95007    <-----object o construction 
The distructor was called: 0x7ffda9d94fe7                         <-----object i destruction
The distructor was called: 0x7ffda9d95007                         <-----object o destruction

This time since we have supplied the -fno-elide-constructors flag to the compiler, NRVO is disabled. This means that now the compiler cannot omit the copy/move construction corresponding to the return statement return i;. This in turn means that first the object i will be constructed using the converting constructor test::test(int) and thus we see the very first line in the output.

Next, this local variable named i will be moved using the move constructor test::test(test&&) and hence we see the second line of the output. Note that the object o will be constructed directly from this moved prvalue directly due to mandatory copy elison since you're using C++17.

Next, the local variable i will be destructed using the destructor test::~test() and we see the third line in the output.

Finally, the object o will get destroyed and we see the fourth line of the output.

In this case, it is as-if you wrote:

test o = std::move(test(5)); //equivalent to this
Jason
  • 36,170
  • 5
  • 26
  • 60
  • How does in your code : [link](https://wandbox.org/permlink/W4WukRIwHIh4zAym) the flag -fno-elide-constructors have an effect on the output although you use C++17 ( from C++17 onwards, the flag -fno-elide-constructors won't have any effect on the output ) ? and note that if you try the same code with C++14 you will have 3 constructors – f877576 May 26 '22 at 05:52
  • @f877576 In C++17 there is mandatory copy elison and thus we see only 2 calls to the constructor with the flag but in C++14 and C++11 there is non-mandatory copy elison and thus we see 3 calls to the constructor with flag . The flag have an effect on the output in C++17 when `i` is returned using `return i;` because this include NRVO which is still non-mandatory. In particular, in C++17 `o` will be directly constructed and hence no third call to the ctor but **prior to** C++17 there will be a third call to the ctor because `o` will be **not be directly constructed** from the moved prvalue. – Jason May 26 '22 at 06:14
  • @f877576 Note that this extra third call to the constructor is happening in C++14 due to 2 reasons: **1)** We have used `-fno-elide-constructors` **2)** In C++14, `o` is not directly constructed from the moved prvalue. But instead it(`o`) is constructed using the move constructor and hence if you notice you will see the this third call is to the move constructor. – Jason May 26 '22 at 06:20
  • Okay what I understand from your two comments is : there is two of ( `optmizations` ) `Mandatory` and `NON-Mandatory` and in `C++17`and above `elision copy` is `Mandatory` and `NRVO` is `NON-Mandatory` so when you use `-fno-elide-constructors` with `C++17` and above only the `NRVO` will be disabled AND in `C++14` `elision copy` is `NON-Mandatory` and `NRVO` is also `NON-Mandatory` so when you use `-fno-elide-constructors` with `C++14` the `NRVO ` will be disabled and the `elision copy` will be also disabled Am I totally understanding your two comments correctly ? @Anoop Rana – f877576 May 29 '22 at 22:29
  • @f877576 Yes, you got it basically. – Jason May 30 '22 at 03:19
  • in this sentence " ( the sentence number 1 ) There is two of ( `optmizations ` ) " I meant " ( the sentence number 2 ) There are two types of ( `optimizations` ) " but the auto correction changed it . if we delete ** the sentence number 1 ** ( from my comment ) and write instead of it ** the sentence number 2 ** **( without change anything else in my comment ) ** your answer of my question ( " Am I totally understanding your two comments correctly ? " ) will still yes ? @Anoop Rana – f877576 May 30 '22 at 06:28
  • @f877576 Your last comment was unclear to me. So let me summarize and then you can tell if you meant the same. In C++14 there were 2 optimizations called NRVO and RVO. And both of these were non-mandatory in C++14 and C++11. But from C++17, RVO became mandatory and so it(RVO) is no longer considered an optimization. But NRVO is still an optimization in C++17 and is still non-mandatory in C++17. Also, do refer to [mandatory copy elison](https://en.cppreference.com/w/cpp/language/copy_elision) for full details in particular the second bullet point about initialization of object with prvalue. – Jason May 30 '22 at 07:20
  • Whatever we use C++17 or C++14 . if we use `-fno-elide-constructors` non-mandatory will be disabled but mandatory will not be disabled , right ? @Anoop Rana – f877576 May 30 '22 at 07:44
  • @f877576 Yes, exactly. The flag `-fno-elide-constructors` will not have any effect on anything that is mandatory. On the other hand, if something(some optimization) is non-mandatory then the flag `-fno-elide-constructors` will have effect on it in both C++14 and C++17 as well. – Jason May 30 '22 at 07:45
  • I am sorry for bothering you . in this question [link](https://stackoverflow.com/questions/71865918/why-isnt-rvalue-copy-constructor-used-for-assignment-from-temporary) there is copy elision happens . Does this copy elision have an abbreviation ( like NRVO , RVO ) ? @Anoop Rana – f877576 Jun 02 '22 at 15:57
  • Thank you and I am sorry for bothering you @Anoop Rana – f877576 Jun 02 '22 at 16:13
  • @f877576 The copy elision happening [there](https://stackoverflow.com/a/71866714/12002570) does not have an abbreviation. Moreover, prior to C++17 it is non-mandatory. But from C++17 it became mandatory. Note that there your assignment `operator=` has a parameter `test e`. So we're talking about this parameter `e`'s initialization.This can be explained by the [this in C++11](https://ibb.co/YN62125) and [this in C++17](https://ibb.co/YQRbsX0). The images are the 2nd bullet point from [copy elison](https://en.cppreference.com/w/cpp/language/copy_elision#Mandatory_elision_of_copy/move_operations) – Jason Jun 02 '22 at 16:37
  • When you said " Note that there your assignment `operator=` has a parameter test e. So we're talking about this parameter `e` 's initialization " you only meant that the copy elision happens because the `operator=` takes `test e` as a parameter ( pass by value ) ( if the operator= takes `test &e` or `test &&e` as a parameter the copy elision does not happen because it is ( `test &e` or `test &&e` ) pass by reference not pass by value ) or there is anything you want me to note it when you mentioned this sentence ? @Anoop Rana – f877576 Jun 02 '22 at 16:58
  • @f877576 Yes, I meant the same as you understood. That we have `test e`(by value) and not `test &e`(by reference). – Jason Jun 03 '22 at 03:10
  • What is the difference between your last two comments ? @Anoop Rana – f877576 Jun 03 '22 at 03:16
  • @f877576 Nothing, i just thought you didn't see the previous one. So i added a second one. I have deleted one of them now. – Jason Jun 03 '22 at 03:20
  • Can we be more specific in the none optimised version about when the destructor for i is called. Is that at the end of the function() or the end of main() . In this example it doesn't make much of a difference because function() is on the last line of main but assuming it wasn't – Darrell123 Aug 30 '22 at 14:34
  • @Darrell123 In the case of *without optimization*, the destructor for `i` will be called before the destructor for `o`. See the comments in that part of my answer. I've already explained it in detail by using arrows. – Jason Aug 30 '22 at 14:49
  • @JasonLiam The answer just says i is destructed before o, that not a complete answer to my question. I want to know when i is destructed independent of what is happening to o. I.e. when does the compiler consider i out of scope and destroy it – Darrell123 Aug 30 '22 at 15:16
  • 1
    @Darrell123 The point is that the destructor for `i` is called after the move construction. This is independent of `o`. You can even remove `o` from the program. – Jason Aug 30 '22 at 15:27