I am working on C++ STL recently, and I will discuss an example using deque
. Note that deque
is just an example, we could also use vector
or others.
The minimal reproducible example is a simple multi-queue producer-consumer problem. Consider a smart washer who can do the laundry itself, and a smart dryer who can take the clothes from the washer and dry them. The washer can be considered a queue of multiple unwashed clothes, while the dryer can be considered as a queue of washed clothes.
Let class Clothes
be a unit of clothes. The producer will keep generating Clothes
and add them to the washer queue. When the washer finishes one Clothes
, it will remove it and add it to the dryer queue.
If we were to do this in C, it would be simple by using pointers (malloc()
) and a linked list. By moving the pointers around we are guaranteed that no duplicated Clothes
are allocated. When we finished, we could simply call free()
.
Can we do the same thing in C++ STL? What would be the recommended approach to achieve that?
I ran some tests and found that C++ STL handles the memory allocation and deallocation itself, which makes me really confused.
For instance, (suppose we use deque
in this case):
std::deque<Clothes> washer = {...}; // only 1 object
std::deque<Clothes> dryer; // empty
// case 1: copy constructor is invoked, 2 memory locations occupied
Clothes c = washer.front();
washer.pop_front(); // destructor is invoked, I think this corresponds to the original Clothes
The above invokes the copy constructor and results in 1 duplicated copy. The operation pop_front()
implicitly invokes the destructor of the Clothes
object. But it will not influence c
as it is constructed by the copy constructor. Now, the number of Clothes
is still one.
// case 2: copy constructor is not invoked
Clothes &c = washer.front();
washer.pop_front(); // destructor is invoked again, but there is only one Clothes object
The above code obtains the first object in washer
by reference, and the copy constructor is not invoked. I would assume that there is still only one Clothes
object now. However, when we call pop_front()
, the destructor of Clothes
is invoked. I tried to access some fields in Clothes
object by c.val
(suppose val
is a field in Clothes
), and I found that I could still get access to it, meaning Clothes c
was not destructed. That is why I am confused. Why is the destructor invoked, and which Clothes
object was destructed?
I think case 1 is likely to be the correct approach, but case 2 seems to be correct as well, although some unexpected things happened.
The issues in your code:
You are correct that in case 1 a copy of Cloths
is created.
You are also correct that in case 2 c
no copy is made.
However, in case 2 c
is a dangling reference to a destructed object, and accessing it causes undefined behavior. This means that anything can happen, including the appearnce of "working" code.
A solution:
C++ offers smart-pointers.
You can use std::shared_ptr<Clothes>
as an element in your containers. This will avoid both copying of the Clothes
objects and having a dangling reference.
A std::shared_ptr
manages the lifetime of the object (of type Clothes
in this case) automatically. It will be released when no longer needed (this is done by keeping a reference-count for each object).
Note that the recomended way to create shared_ptr
s is via std::make_shared
.
#include <deque>
#include <memory>
class Clothes
{
// ...
};
int main()
{
std::deque<std::shared_ptr<Clothes>> washer = { std::make_shared<Clothes>() };
std::deque<std::shared_ptr<Clothes>> dryer;
auto c = washer.front();
washer.pop_front();
dryer.push_back(c);
}
Finally note that there is also std::unique_ptr
, which supports a single owner and no copying (only moving). However in this case it seems to me that std::shared_ptr
is more straight-forward to use. If you want to use std::unique_ptr
, it should be something like:
#include <deque>
#include <memory>
class Clothes
{
// ...
};
int main()
{
std::deque<std::unique_ptr<Clothes>> washer;
washer.push_back(std::make_unique<Clothes>());
std::deque<std::unique_ptr<Clothes>> dryer;
auto c = std::move(washer.front());
washer.pop_front();
dryer.push_back(std::move(c));
}