samedi 6 juin 2020

Why return by shared_ptr instead of by value when RVO could apply?

I’ve recently started reading through “C++ Concurrency in Action”, and I’ve been surprised by how much I’ve learned about the features of C++ that are totally independent of the concurrency API. There’s one example that I can’t figure out, though: several of the example concurrent data structure in the book return by shared_ptr instead of by value when RVO could apply. (I am definitely a C++ novice, though, so perhaps the real issue is my understanding of RVO.)

The book has this to say about its threadsafe queue: “Copying the std::shared_ptr<> out of the internal std::queue<> then can’t throw an exception, so wait_and_pop() is safe again”. The book seems to imply that having an internal queue of data values rather than an internal queue of shared_ptrs could lead to issues if the value being popped is returned to the caller only after the queue has been modified. If process of copying the data to return to the caller threw an exception, the data just popped would be lost but the stack would have still been modified.

Let me paste a direct quote from the textbook in case I’m misinterpreting what they’re saying:

“For those of you who aren’t aware of the issue, consider a stack<vector<int>>. Now, a vector is a dynamically sized container, so when you copy a vector the library has to allocate some more memory from the heap in order to copy the contents. If the system is heavily loaded, or there are significant resource constraints, this memory allocation can fail, so the copy constructor for vector might throw a std::bad_alloc exception. This is especially likely if the vector contains a lot of elements. If the pop() function was defined to return the value popped, as well as remove it from the stack, you have a potential problem: the value being popped is returned to the caller only after the stack has been modified, but the process of copying the data to return to the caller might throw an exception. If this happens, the data just popped is lost; it has been removed from the stack, but the copy was unsuccessful! The designers of the std::stack interface helpfully split the operation in two: get the top element (top()) and then remove it from the stack (pop()), so that if you can’t safely copy the data, it stays on the stack. If the problem was lack of heap memory, maybe the application can free some memory and try again.”

The steps taken by the pop() function in question are:

  1. Get the topmost value and store it as a local variable

  2. Pop the value from the stack

  3. Return the value

My question is: how could this exceptional condition occur with return value optimization? If the conditions for copy elision are met, but compilers choose not to perform copy elision, the object being returned must be treated as an rvalue. In effect, the Standard requires that when the RVO is permitted, either copy elision takes place or std::move() is implicitly applied to local objects being returned. The situation described above definitely meets the condition for RVO. Therefore, the cost of moving the object would either be a single move (no RVO, object treated as an rvalue) or nothing at all (RVO). So is moving the value out by shared_ptr really necessary?

Aucun commentaire:

Enregistrer un commentaire