Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Shouldn't NRVO guarantee the local named variable and the call-site variable to take the same address?

Tags:

c++

nrvo

I think it should, because it's important for correctness. However, I'm surprised to see Clang's output. Consider the code below:

#include <iostream>

struct S
{
    int i;

    S(int i) : i(i) {}

    S(S&&)
    {
        std::cout << "S(S&&)\n";
    }

    S(S const&) = delete;
};

S f()
{
    S s{42};
    std::cout << &s << "\n";
    return s;
}

int main()
{
    S s{f()};
    std::cout << &s << "\n";
    std::cout << s.i << "\n";
}

We defined a move ctor for S to check if S(S&&) is called, if not, NRVO is applied.

The result we see from GCC is:

0x7ffc3ed7b5ac
0x7ffc3ed7b5ac
42

NRVO is applied and they take the same address, which is expected.

However, Clang's output:

0x7fff908bbcc8
0x7fff908bbcf8
42

NRVO is applied but the addresses differ.

In case you wonder why having the same address is important - it's because some object may do some registration with its address at construction, and if the object is moved, it should be notified (e.g. via move-ctor).

Having NRVO applied but with different memory address thus makes it ill-formed. It's a clear violation of the contract - no custom move/copy ctor is called, how could the compiler "copy" the data of S to a different place?

Is this a bug in Clang?


If we add a destructor to S, e.g.

~S() {}

This time, Clang outputs the same address.

like image 248
Jamboree Avatar asked Jul 09 '17 14:07

Jamboree


1 Answers

Definitely seems to be a bug in clang, they should be the same, else things like the following will be erroneous

struct S
{
    int i;
    int* ptr;

    S(int i) : i(i) {
        this->ptr = &this->i;
    }

    S(S&& s)
    {
        this->i = s.i; 
        this->ptr = &this->i;
        std::cout << "S(S&&)\n";
    }

    S(S const&) = delete;
};

Where a move (or elision where addresses don't change) is required to ensure that the internal pointer points to the correct integer. But because of elision that pointer points to memory that does not contain the member integer.

See output here https://wandbox.org/permlink/NgNR0mupCfnnmlhK

As pointed out by @T.C., this is actually a bug in the Itanium ABI spec that doesn't take move-ctor into account. Quoting from Clang's dev:

Clang's rule is the one in the ABI: a class is passed indirectly if it has a non-trivial destructor or a non-trivial copy constructor. This rule definitely needs some adjustment [...]

Indeed, if we define either a non-trivial dtor or copy-ctor for S in the original example, we get the expected result (i.e. same address).

like image 199
Curious Avatar answered Nov 20 '22 05:11

Curious