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

257 views Asked by At

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

2

There are 2 answers

2
Miles Budnek On BEST ANSWER

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.

4
Ted Lyngmo On

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?

There is no RVO (which is mandatory) here, but possibly NRVO (named return value optimization), which is not mandatory, so yes, it could very well be dangling if you enabled the copy constructor. Without it, it'll fail to link in case NRVO isn't selected.

Note: This is true in C++17 too! NRVO does not mandate that there will be copy/move elision of x -> b and if you = delete; the copy constructor, it will not compile even in 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?

For C++17, that's correct. For C++14, the selected constructor must be accessible even if the call is elided, so in your case, it would fail to link if elision isn't selected by the compiler. If you = delete; the copy constructor, it'll fail to compile, even if it could elide the copy.