I write this code:
#include <iostream>
using namespace std;
class Foo
{
public:
int a = 0;
Foo()
{
cout << "ctor: " << this << endl;
}
~Foo() {
cout << "dtor: " << this << endl;
}
};
Foo f()
{
Foo foo;
cout << "f " << &foo << endl;
return foo;
}
void ff(Foo &&ffoo)
{
cout << "ff " << &ffoo << endl;
}
int main()
{
ff(f());
std::cout << "Hello World!n";
}
The output looks fine:
ctor: 0x7ffeda89bd7c
f 0x7ffeda89bd7c
ff 0x7ffeda89bd7c
dtor: 0x7ffeda89bd7c
but when I delete the ~Foo()
like this:
#include <iostream>
using namespace std;
class Foo
{
public:
int a = 0;
Foo()
{
cout << "ctor: " << this << endl;
}
// ~Foo() {
// cout << "dtor: " << this << endl;
// }
};
Foo f()
{
Foo foo;
cout << "f " << &foo << endl;
return foo;
}
void ff(Foo &&ffoo)
{
cout << "ff " << &ffoo << endl;
}
int main()
{
ff(f());
std::cout << "Hello World!n";
}
I got this output:
ctor: 0x7fffd5c8bf4c
f 0x7fffd5c8bf4c
ff 0x7fffd5c8bf6c
Hello World!
Why is the address of ffoo
different from f
? Isn’t it supposed to be the same?
The compile cmd is:
g++ tmp.cpp
g++ --version
g++ (Debian 8.3.0-6) 8.3.0
Copyright (C) 2018 Free Software Foundation, Inc.
2
Answers
You will have the same address only when NRVO applies.
But NRVO is not guaranteed.
It happens that with your changes, NRVO happens in one case and not for the other, but in both versions, NRVO could happen.
As @NateEldredge pointed to in comments, this problem is related to the applied ABI. I suppose that, in your case, it is the System V ABI.
The difference between the two versions of your class
Foo
is in their triviallity. In the first case,Foo
has a user-provided destructor and, consequently, the class is not trivial. In the second case, the opposite holds.Now, what is the difference according to the ABI? Basically, it says that objects of trivial types up to some size of their binary representation are passed/returned by value between functions in registers. The C++ Standard explicitly allows that in [class.temporary/3]:
This allows objects to be stored only in registers during their lifetime, such that they don’t need to be stored in (slow) memory. However, in your case, since you are evaluating their addresses, they must be eventually stored in memory. This happens both inside and outside of the function
f
. Due to the passing of the return value in a register, there is no other option than to store explicitly both objects on the stack, once in the stack frame off
and once in the stack frame of the calling function. This implies that both objects have different addresses.Now, when you provide a custom destructor, the type is no longer trivial. According to the ABI, it now needs to be passed in memory. For returning by value, this works such that the caller allocates the storage for the return value on its stack and passes its address as a hidden argument; quoting form the ABI document:
This means that the function has internally an access to this storage. Now, it has two options. Without optimizations, the function can first create a separate object in its stack frame and finally copy/move-construct it into the storage pointed to by
rdi
. The second option is to work directly with this storage, which allows elision of the copy/move, and this optimization is referred to as (N)RVO.Note that the same outcome will be achieved with a trivial version of your class once you make it large enough such that its binary representation will no more fit into a register.
TL;DR The difference between both versions of your class is in its triviallity. When the class is trivial (and small enough), the ABI prescribes its passing in registers (instead of in memory). When passing in registers, objects inside and outside of the function are stored in different stack frames and, therefore, have different addresses. On the contrary, when passing in memory, both inside and outside of the function it is worked with a single storage (allocated in the caller’s stack frame).