Consider the following program:
#include <functional>
#include <iostream>
class RvoObj {
public:
RvoObj(int x) : x_{x} {}
RvoObj(const RvoObj& obj) : x_{obj.x_} { std::cout << "copied\n"; }
RvoObj(RvoObj&& obj) : x_{obj.x_} { std::cout << "moved\n"; }
int x() const { return x_; }
void set_x(int x) { x_ = x; }
private:
int x_;
};
class Finally {
public:
Finally(std::function<void()> f) : f_{f} {}
~Finally() { f_(); }
private:
std::function<void()> f_;
};
RvoObj BuildRvoObj() {
RvoObj obj{3};
Finally run{[&obj]() { obj.set_x(5); }};
return obj;
}
int main() {
auto obj = BuildRvoObj();
std::cout << obj.x() << '\n';
return 0;
}
Both clang and gcc (demo) output 5
without invoking the copy or move constructors.
Is this behavior well-defined and guaranteed by the C++17 standard?
The principle that objects own resources is also known as "resource acquisition is initialization," or RAII. When a resource-owning stack object goes out of scope, its destructor is automatically invoked. In this way, garbage collection in C++ is closely related to object lifetime, and is deterministic.
Copy elision is an optimization implemented by most compilers to prevent extra (potentially expensive) copies in certain situations. It makes returning by value or pass-by-value feasible in practice (restrictions apply).
Copy elision only permits an implementation to remove the presence of the object being generated by a function. That is, it can remove the copy from obj
to the return value object of foo
and the destructor of obj
. However, the implementation can't change anything else.
The copy to the return value would happen before destructors for local objects in the function are called. And the destructor of obj
would happen after the destructor of run
, because destructors for automatic variables are executed in reverse-order of their construction.
This means that it is safe for run
to access obj
in its destructor. Whether the object denoted by obj
is destroyed after run
completes or not does not change this fact.
However, there is one problem. See, return <variable_name>;
for a local variable is required to invoke a move operation. In your case, moving from RvoObj
is the same as copying from it. So for your specific code, it'll be fine.
But if RvoObj
were, for example, unique_ptr<T>
, you'd be in trouble. Why? Because the move operation to the return value happens before destructors for local variables are called. So in this case obj
will be in the moved-from state, which for unique_ptr
means that it's empty.
That's bad.
If the move is elided, then there's no problem. But since elision is not required, there is potentially a problem, since your code will behave differently based on whether elision happens or not. Which is implementation-defined.
So generally speaking, it's best not to have destructors rely on the existence of local variables that you're returning.
The above purely relates to your question about undefined behavior. It isn't UB to do something that changes behavior based on whether elision happens or not. The standard defines that one or the other will happen.
However, you cannot and should not rely upon it.
Short answer: due to NRVO, the output of the program may be either 3
or 5
. Both are valid.
For background, see first:
Guideline:
For example, when we see the following pattern:
T f() {
T ret;
A a(ret); // or similar
return ret;
}
We need to ask ourselves: does A::~A()
modify our return value somehow? If yes, then our program most likely has a bug.
For example:
[From https://stackoverflow.com/a/54566080/9305398 ]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With