Copy constructor is not called when return by value
Asked Answered
I

2

10

I was playing around with C++ constructors. Here is my code:

#include <iostream>
using namespace std;

class ArrayWrapper
{
public:
    // default constructor produces a moderately sized array
    ArrayWrapper ()
        : _p_vals( new int[ 64 ] )
        , _size( 64 )
    {
        cout << "Default constructor: " << this << endl;
    }

    explicit ArrayWrapper (int n)
        : _p_vals( new int[ n ] )
        , _size( n )
    {
        cout << "Constructor: " << this << endl;
    }

    // move constructor
    ArrayWrapper (ArrayWrapper&& other)
        : _p_vals( other._p_vals  )
        , _size( other._size )
    {
            cout << "Move constructor: " << this << endl;
            cout << "Move from: " << &other << endl;
            other._p_vals = NULL;
            other._size = 0;
    }

    // copy constructor
    ArrayWrapper (const ArrayWrapper& other)
        : _p_vals( new int[ other._size  ] )
        , _size( other._size )
    {
            cout << "Copy constructor: " << this << endl;
            for ( int i = 0; i < _size; ++i )
            {
                    _p_vals[ i ] = other._p_vals[ i ];
            }
    }
    ~ArrayWrapper ()
    {
            cout << "Destructor: " << this << endl;
            delete [] _p_vals;
    }

public:
    int *_p_vals;
    int _size;
};

ArrayWrapper foo() {
    ArrayWrapper a(7);
    cout << "Temp object created!" << endl;
    return a;
}


int main() {
    ArrayWrapper b(foo());
    cout << "Finish!" << endl;
}

The output is:

Constructor: 0x7fff5d97bb60
Temp object created!
Destructor: 0x7fff5d97bb60
Move constructor: 0x7fff5d97bbd0
Move from: 0x7fff5d97bbc0
Destructor: 0x7fff5d97bbc0
Finish!
Destructor: 0x7fff5d97bbd0

The first three line indicates that the local variable in foo() function is created with constructor, and destroyed when foo() returns. The 4th line indicates that b is constructed using move constructor. But, the next two lines are most confusing: I now have a new address, that is different from the local variable "a" in foo(), that I used to call the move constructor. When the copy constructor finishes, the rvalue reference vanishes, and destructor is called. But why isn't there a copy constructor for 0x7fff5d97bbc0? In other words, where does 0x7fff5d97bbc0 come from and how is it constructed? It is simply wired that there is one more destructors called than constructors called.

I got a feeling that this has something todo with copy elision. Thus I changed the return line in foo() to the following:

return std::move(a);

And the output is:

Constructor: 0x7fff55a7ab58
Temp object created!
Copy constructor: 0x7fff55a7abc0
Destructor: 0x7fff55a7ab58
Move constructor: 0x7fff55a7abd0
Move from: 0x7fff55a7abc0
Destructor: 0x7fff55a7abc0
Finish!
Destructor: 0x7fff55a7abd0

Now it finally made some sense: on the third line, it shows that copy constructor is called before "a" is destroyed. This means, when returning by value, it actually copied the value into the return value before destroy the temporary variable.

But I still got confused by the original program (without std::move()), because if it is really caused by copy elision, shouldn't foo()'s return value's address be the same with the local variable "a"? Now that it is different, which means it locates in a total different position in the memory from "a", then why didn't it call the copy constructor?

Hope my question is clear and understandable.

-------------------------------------------------------------------------------

Edit: the compiler I used was clang++ with -fno-elide-constructors flag.

Invasion answered 30/1, 2014 at 21:35 Comment(24)
Can you please post the compiler in the question? It seems like nobody can exactly reproduce your results. I've also tried vc++ on RextesterGirovard
The point of the move constructor is that it can move the object members from foo.a to main.b. So no copy is required. Add cout << "main.b located at " << &b << endl; to main() to see this.Foreground
I used clang++ with -fno-elide-constructors flag.Invasion
@HansPassant I added the line as you said and got a even wireder output: after everything is output, there is an error msg: a.out(17285,0x7fff7a537310) malloc: *** error for object 0x7fff5c49bc20: pointer being freed was not allocated *** set a breakpoint in malloc_error_break to debug Abort trap: 6Invasion
@Tomás Badan There is something weirder, i tried by deleting the operator= and there is still a invisible copy in the version with copy elision disabled, the log shows 2 constructors and 3 destructors : coliru.stacked-crooked.com/a/de9c5b3a410a5084Allergen
@Allergen exactly! There is one more destructors called than constructors called (if std::move() is not added).Invasion
Look here to see why the message "Copy constructor" is not being displayed: https://mcmap.net/q/354956/-copy-constructor-elision-duplicate where he said: The C++ standard allows an implementation to omit a copy operation resulting from a return statement, even if the copy constructor has side effects. Nervine
@TomásBadan It does, but that's not the problem here. Notice that in first output there's more destructor calls than constructor call and that makes no sense.Unasked
@TomásBadan So what you are saying is that the copy constructor is NOT called at all here, right? That means the return value of foo() is NOT constructed by the copy constructor. But how IS it constructed?Invasion
@ShiningSun First output is nonsense. I don't know the reason for it, but it's definitely a bug in the compiler.Unasked
No, it is saying that b is being constructed, by copy elision, directly from object ArrayWrapper a(7)Nervine
@Unasked thats explain his original question. Now, arise a new question, why 3 destructor.Nervine
@TomásBadan Because, as I said, the compiler has a bug.Unasked
@TomásBadan Sorry for not clearly stated the question. The ultimate question here is why there is one more destructor.Invasion
Look at my edited answer, even the pointer deleted is garbage in the nocopy elision version, depends on the existence of an additional int member.Allergen
@Allergen Thank you for your effort!Invasion
Isn't this just a case of RVO? (Return Value Optimization)Nutt
@ShiningSun I have submitted a bugreport.Execute
@EJP I considered it and that is why I added the -fno-elide-constructors flag, which is supposed to prevent it...Invasion
@ShiningSun Thank YOU for posting this question, together with a compilable code!Execute
@ShiningSun OK, Richard Smith says this bug has already been fixed in trunk.Execute
@Execute Thrilled to hear that!Invasion
@ShiningSun Actually, I can personally confirm that: I have just finished building the latest clang from trunk and it works as expected!Execute
So, no answer to this question ? plus, I have seen the same code on cprogramming.com/c++11/… maybe a citation would be a good point, unless you are the original author of this page.Detector
A
3

What is your compiler, clang without the std::move:

Constructor: 0x7fff0b8e3b80
Temp object created!
Finish!
Destructor: 0x7fff0b8e3b80

with the std::move:

Constructor: 0x7fffca87eef0
Temp object created!
Move constructor: 0x7fffca87ef30
Move from: 0x7fffca87eef0
Destructor: 0x7fffca87eef0
Finish!
Destructor: 0x7fffca87ef30

This two results are far more logical than yours, so again, what is your compiler ?

Edit : It tastes like a bug with the -fno-elide-constructors flag.

by adding an int member after the two original members, same result, but if the int is first, memory corruption ! And the non corruption version ends with a nullptr value in the main ArrayWrapper. See the " and delete " log to catch the erroneous behavior.

http://coliru.stacked-crooked.com/a/f388c504b442b71d <- int after, ok

http://coliru.stacked-crooked.com/a/9beced1d5a2aa6e4 <- int before, corruption dump

Allergen answered 30/1, 2014 at 21:49 Comment(4)
I used clang++ with -fno-elide-constructors flagInvasion
Cool! But is this the bug that caused my problem?Invasion
@Allergen I have submitted a bugreport.Execute
OK, Richard Smith says this bug has already been fixed in trunk.Execute
M
0

Looks like you get there a lot of copies, with g++4.8 I get following output:

Constructor: 0x7fff88925df0
Temp object created!
Finish!
Destructor: 0x7fff88925df0

if I add: -fno-elide-constructors then I get :

Constructor: 0x7fff1bd329b0   for this line: ArrayWrapper a(7);
Temp object created!
Move constructor: 0x7fff1bd329f0  for temporary
Move from: 0x7fff1bd329b0  moving from this : ArrayWrapper a(7)
Destructor: 0x7fff1bd329b0   foo is ending so destroy: ArrayWrapper a(7);
Move constructor: 0x7fff1bd329e0  for ArrayWrapper b - it is being created
Move from: 0x7fff1bd329f0  moving from temporary
Destructor: 0x7fff1bd329f0 destroying temporary
Finish!
Destructor: 0x7fff1bd329e0  destroy object b

http://coliru.stacked-crooked.com/a/377959ae1e93cdc9

Melt answered 30/1, 2014 at 21:51 Comment(4)
I used clang++ with -fno-elide-constructors. If without -fno-elide-constructors, I get the same output with your first output, and I suppose it comes from copy elision. But I have no idea how to explain your second output...Invasion
I have added descriptions how I understand each lineMelt
I see. Your output is clearer. So what happened here is that, the return value of foo() is also generated by move constructor, right? Fascinating...But how come clang++ does not show this...Invasion
Yes, because with NRVO disabled temporary is treated as xvalue (eXpiring value) which can be used in move semantics implicitly.Melt

© 2022 - 2024 — McMap. All rights reserved.