skip to Main Content

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


  1. 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.

    Login or Signup to reply.
  2. 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]:

    When an object of class type X is passed to or returned from a function, if X has at least one eligible copy or move constructor ([special]), each such constructor is trivial, and the destructor of X is either trivial or deleted, implementations are permitted to create a temporary object to hold the function parameter or result object. The temporary object is constructed from the function argument or return value, respectively, and the function’s parameter or return object is initialized as if by using the eligible trivial constructor to copy the temporary (even if that constructor is inaccessible or would not be selected by overload resolution to perform a copy or move of the object).

    [Note 4: This latitude is granted to allow objects of class type to be passed to or returned from functions in registers. — end note]

    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 of f 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:

    If the type has class MEMORY, then the caller provides space for the return value and passes the address of this storage in %rdi as if it were the first argument to the function. In effect, this address becomes a “hidden” first argument.

    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).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search