Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it safe to modify RVO values within an RAII construct? [duplicate]

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?

like image 651
Brian Rodriguez Avatar asked Jan 29 '20 21:01

Brian Rodriguez


People also ask

How does RAII work?

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.

What is copy elision in Javascript?

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


2 Answers

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.

like image 73
Nicol Bolas Avatar answered Sep 30 '22 20:09

Nicol Bolas


Short answer: due to NRVO, the output of the program may be either 3 or 5. Both are valid.


For background, see first:

  • in C++ which happens first, the copy of a return object or local object's destructors?
  • What are copy elision and return value optimization?

Guideline:

  • Avoid destructors that modify return values.

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:

  • A type that prints the return value on destruction is fine.
  • A type that computes the return value on destruction is not fine.

[From https://stackoverflow.com/a/54566080/9305398 ]

like image 26
Acorn Avatar answered Sep 30 '22 20:09

Acorn