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 limited 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<void>
. I read some posts in internet but I couldn't solve my doubt
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:
std::shared_ptr
owning the object is destroyed;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
.