Search code examples
c++vectorstldeque

How to add pointer semantics into C++ STL?


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.

Background

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.

Question

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?

What I have tried and what makes me confused

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?

My thoughts

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.


Solution

  • 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_ptrs 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));
    }