Search code examples
c++effective-c++

Can I cache after using std::make_shared?


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_ptrs and weak_ptrs 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_ptrs 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;
}

Solution

  • 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.