How is memory managed during C++ copy elision?

Viewed 161
#include <iostream>

using namespace std;

class A
{
public :
    A()
    {
        cout<<"constructor is called"<<endl;
    }
    
    ~A()
    {
        cout<<"destructor is called"<<endl;
    }
    
    A(const A &s)
    {
        cout<<"copy constructor is called"<<endl;
    }
};

A beta()
{
    A a;
    cout<<"mem location a : "<<&a<<endl;
    return a;
}

int main(int argc, char** argv) {
    A b = beta();
    cout<<"mem location b : "<<&b<<endl;
    return 0;
}

The above program generates the following output :

constructor is called
mem location a : 0x7ffc12bdaf77
mem location b : 0x7ffc12bdaf77
destructor is called

As far as I understand just a single instance of A was created rather than 2 instances for A a and A b, because of copy elision or return value optimisation. But looking at above program from a memory perspective, object a is inside the stack activation record or stack space of function beta and has a memory location 0x7ffc12bdaf77 . When it returns from beta, copy elision makes object b just same as object a rather than copying it. So, now b also has the address 0x7ffc12bdaf77 . I am unable to understand if b is still a local of the function main and present inside its stack space, how can it occupy a memory outside main's stack space ?

3 Answers

Consider this line: A b = beta();. How does a compiler implement that?

Well, beta is some other function. And beta has a return value that is an A. So there are two possible ways to implement this. The compiler could make beta allocate the stack space for its A return value, but that could be problematic since the caller needs to use that return value. So instead, the compiler makes the caller allocate the stack space for the return value. After all, the caller does know the size/alignment of the return value, so it has everything it needs to know to allocate that space.

So let's go with the latter. This means that when the compiler calls beta, it passes in the address where beta's return value will be. But this also means that the compiler, for this particular call of beta could just give beta's return value the same address as it will give b.

So right there, we've elided the copy from the return value of the function into b.

So when the compiler goes to compile beta, it knows that the caller is going to give it a pointer to where the return value should go. So the return a; statement semantically copies from the a variable into this return value object.

But, the compiler can see the entirety of beta. And it can see that the a variable is a local variable, and it gets returned on all control paths. So instead of giving a a separate stack address, the compiler could just put a in the memory provided by the caller for the return value.

So again, we have elided a copy from a into the return value.

In the microsoft x64 calling convention, a hidden pointer will be prepended as the first argument to beta. This pointer contains the address of b, which will be on main's stack frame. This pointer will be used to immediately create both a and b at the same time on main's stack frame. So in other words, a doesn't exist anywhere on beta's stack frame; from a memory perspective a and b will be equivalent.

As far as I understand just a single instance of A was created rather than 2

Correct.

how can it occupy a memory outside main's stack space ?

It doesn't.

The return value is simply created in the stack frame of the main call.

Related