Why is the copy constructor called twice in this code snippet?
Asked Answered
R

2

14

I'm playing around with a few things to understand how copy constructors work. But I can't make sense of why the copy constructor is called twice for the creation of x2. I would have assumed it would be called once when the return value of createX() is copied into x2.
I also looked at a few related questions on SO, but as far as I can tell I couldn't find the same simple scenario as I am asking here.

By the way, I'm compiling with -fno-elide-constructors in order to see what's going on without optimizations.

#include <iostream>

struct X {
    int i{2};

    X() {
        std::cout << "default constructor called" << std::endl;
    }

    X(const X& other) {
        std::cout << "copy constructor called" << std::endl;
    }
};

X createX() {
    X x;
    std::cout << "created x on the stack" << std::endl;
    return x;
}

int main() {
    X x1;
    std::cout << "created x1" << std::endl;
    std::cout << "x1: " << x1.i << std::endl << std::endl;    

    X x2 = createX();
    std::cout << "created x2" << std::endl;
    std::cout << "x2: " << x2.i << std::endl;    

    return 0;
}

This is the output:

default constructor called
created x1
x1: 2

default constructor called
created x on the stack
copy constructor called
copy constructor called
created x2
x2: 2

Can someone help me what I'm missing or overlooking here?

Rumal answered 28/2, 2019 at 15:37 Comment(0)
D
20

What you have to remember here is that the return value of a function is a distinct object. When you do

return x;

you copy initialize the return value object with x. This is the first copy constructor call you see. Then

X x2 = createX();

uses the returned object to copy initialize x2 so that is the second copy you see.


One thing to note is that

return x;

will try to move x into the return object if it can. Had you made a move constructor you would have seen this called. The reason for this is that since local objects go out of scope at the end of the function, the compiler treats the object as an rvalue and only if that does not find a valid overload does it fall back to returning it as an lvalue.

Dopp answered 28/2, 2019 at 15:41 Comment(6)
@FrançoisAndrieux Yeah. Updating the wording to make that more explicit that isn't something that is just allowed but has to be done.Dopp
I'm not sure of the details of -fno-elide-constructors but I'm assuming it prevents RVO.Pilfer
@FrançoisAndrieux It does not prevent guaranteed RVO if using C++17 since that is mandated that it has to happen, there isn't actually a temporary object created. See here where it only uses a single copy as the second one is mandated not to happen.Dopp
So we have to assume OP is using a pre-C++17 standard?Pilfer
Kind of. The compiler they are using could have a bug, or they are using pre C++17. I'm inclined to believe it is the latter.Dopp
Yes I'm using c++14. I didn't realize it would make a difference. Updated the post.Rumal
S
14

First copy is in return of createX

X createX() {
    X x;
    std::cout << "created x on the stack" << std::endl;
    return x; // First copy
}

Second one is to create x2 from the temporary return by createX.

X x2 = createX(); // Second copy

Notice that in C++17, second copy is forced to be elided.

Sawyor answered 28/2, 2019 at 15:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.