Search code examples
c++shared-ptrstdmap

Problems understanding the use of shared_ptr<void> in std::map


I have the following code:

#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <sstream>

using namespace std;

typedef std::map<std::string, std::shared_ptr<void>> M;
typedef std::vector<std::string> S;

void build_word_tree_from_sentences(const S& sentence_list, M& root)
{
    for (const auto& sentence : sentence_list)
    {
        std::string word;
        std::stringstream ss(sentence);
        M* base = &root;
        while (ss >> word)
        {
            auto found = base->find(word);
            if (found == base->end())
            {
                base->insert(std::make_pair(word, std::make_shared<M>()));
            }

            auto b = base->find(word)->second;
            base = std::static_pointer_cast<M>(b).get();
        }
    }
}

int main()
{
    S sentence_list = {"Hello word", "Hello there"};
    M tree;
    build_word_tree_from_sentences(sentence_list, tree);
}

I understand the use of std::shared_ptr<>, it manages the storage of pointers and provides a limite garbage collection by handling automatic deletion of previously allocated memory. What it's not really clear to me is why it has been declared as std::shared_ptr. I read some posts in internet but I couldn't solve my doubt


Solution

  • Your question requires to study some features of std::shared_ptr before coming to an answer.

    std::shared_ptr is a smart pointer that retains shared ownership of an object through a pointer. Several std::shared_ptr objects may own the same object. The object is destroyed and its memory deallocated when either of the following happens:

    • the last remaining std::shared_ptr owning the object is destroyed;
    • the last remaining std::shared_ptr owning the object is assigned to another pointer.

    The fundamental idea of this and other smart pointers is to allow the client to use them as if they were native pointers, but providing the additional feature of automatic memory management. In particular, they exploit SBRM.

    This mechanism is extremely useful for containers, since they work with objects of value_type and not with eventually external storages that are referenced by the objects. For example, in erase operations, the container destroys the object (in this case, the entire node that contains the object is destroyed and then deallocated). If a native pointer were used to reference an external object, as the pointer is destroyed, the external storage would consequently not be deallocated, leading to a memory leak. Among other things, using a native pointer would not make possible to share resources between different containers as in the case of std::shared_ptr.

    Since the object deletion procedure is type-erased, std::shared_ptr can be specialized for the incomplete type void, which allows the client to refer to any object in the same way as it would be done with a native pointer void*.

    Calling the std::make_shared() function allocates a value-initialized object of specialized type, which is M. In particular, the function offers a small advantage in terms of performance and space usage as it allocates memory for the reference counter together with the object.

    I believe the code was developed to create a std::map that contains objects of type std::map in order to create a tree whose nodes are trees themselves and so on. However, the definition of the mapped_type of a std::map specialization as the specialization itself would have led to a recursion.

    using M = std::map<std::string, M>>; // Error!
    

    Thus, it is necessary to define the mapped_type as a void pointer, which can reference to an object of type M without the need of defining it. Although the use of std::shared_ptr may seem inappropriate, since resource sharing never occurs in the piece of code, it had likely been preferred as std::unique_ptr requires more attention in order to be used with void.