I am reading Effective Modern C++ (Scott Meyers) and trying out something from item 21. The book says a side effect of using std::make_shared
is that memory cannot be freed until all shared_ptr
s and weak_ptr
s are gone (because the control block is allocated together with the memory).
I expected that this would mean that if I keep a cache around holding a bunch of weak_ptr
s that no memory would ever be freed. I tried this using the code below, but as the shared_ptrs are removed from the vector, I can see using pmap that memory is actually being freed. Can anyone explain me where I am going wrong? Or if my understanding is wrong?
Note: the function loadWidget
is not the same as in the book for the purpose of this experiment.
#include <iostream>
#include <memory>
#include <unordered_map>
#include <vector>
#include <thread>
#include <chrono>
class Widget {
public:
Widget()
: values(1024*1024, 3.14)
{ }
std::vector<double> values;
};
std::shared_ptr<Widget> loadWidget(unsigned id) {
return std::make_shared<Widget>();
}
std::unordered_map<unsigned, std::weak_ptr<Widget>> cache;
std::shared_ptr<Widget> fastLoadWidget(unsigned id) {
auto objPtr = cache[id].lock();
if (!objPtr) {
objPtr = loadWidget(id);
cache[id] = objPtr;
}
return objPtr;
}
int main() {
std::vector<std::shared_ptr<Widget>> widgets;
for (unsigned i=0; i < 20; i++) {
std::cout << "Adding widget " << i << std::endl;
widgets.push_back(fastLoadWidget(i));
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
while (!widgets.empty()) {
widgets.pop_back();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
return 0;
}
It is true that when you use std::make_shared
the storage for the new object and for the control block is allocated as a single block, so it is not released as long as there exists a std::weak_ptr
to it. But, when the last std::shared_ptr
is destroyed the object is nonetheless destroyed (its destructor runs and its members are destroyed). It's just the associated storage which remains allocated and unoccupied.
std::vector
allocates storage dynamically for its elements. This storage is external to the std::vector
, it is not part of the object's memory representation. When you destroy a Widget
you also destroy its std::vector
member. That member's destructor will release the dynamically allocated memory used to store its elements. The only memory that can't be release immediately is the control block and the storage for Widget
(which should be sizeof(Widget)
bytes). It does not prevent the storage for the elements of the vector from being released immediately.