c++c++17language-lawyercopy-elisionreturn-value-optimization

Understanding Pointer Behavior and Copy Elision in Object Returns Pre- and Post-C++17


I'm trying to understand how pointers within objects behave when these objects are returned from functions, particularly in relation to copy elision and temporary object creation in C++ standards before and after C++17. Below is my code example:

struct A {
    void* p;
    A() : p(this) {}
    A(const A&); // Disable trivial copyability
};

A f() {
    A x;
    return x;
}

int main() {
    A a;  // OK: a.p points to a
    A b = f(); // Concern: b.p could be dangling and point to the x inside f
    A c = A(); // (until C++17) Concern: c.p could be dangling and point to a temporary
               // (since C++17) OK: c.p points to c; no temporary is involved
}

Specifically, my concerns are:

In A b = f();, is it correct that b.p could be a dangling pointer pointing to x inside f(), depending on whether the compiler applies RVO before C++17?

For A c = A();, I understand that in C++17, c.p points directly to c with no temporary involved. But prior to C++17, could c.p be a dangling pointer if a temporary object is created and then destroyed?

I would appreciate any clarification or insights into how these scenarios are handled in different C++ standards.

P.S. I specifically meant C++11 and C++14 as this code won't compile in C++03


Solution

  • When a copy is elided, it means that the the source and destination objects are the same object. There is no copy; that's the whole point.

    In A b = f();, is it correct that b.p could be a dangling pointer pointing to x inside f(), depending on whether the compiler applies RVO before C++17?

    No. Because A's copy constructor isn't implemented, the only way for it to possibly be returned from f is via NRVO, in which case b and x are the same object. Behind the scenes main passes a pointer to f into which f initializes its return object. When (N)RVO is applied, main and f simply both use that storage directly as b and x, respectively, instead of f copying x into the return value and then main copying from the return value to b.

    For A c = A();, I understand that in C++17, c.p points directly to c with no temporary involved. But prior to C++17, could c.p be a dangling pointer if a temporary object is created and then destroyed?

    No. Because A's copy constructor isn't implemented there's no way for a temporary to be copied to c. That means that the only way the program could possibly compile is if the copy is elided and c is directly initialized.


    Note that I don't think it's a good idea to rely on this behavior. It would be very easy for someone to get a random link error (because NRVO didn't get applied in some situation) and change the copy constructor to A(const A&) {} to "fix" the problem, which could easily lead to dangling pointers.