Click here to Skip to main content
15,886,362 members
Articles / Programming Languages / C++11

How a weak_ptr Might Prevent Full Memory Cleanup of Managed Object

Rate me:
Please Sign up or sign in to vote.
5.00/5 (8 votes)
30 Jan 2018CPOL6 min read 9.2K   5   1
It appears that in some cases memory allocated for the object controlled by smart_ptr might not be released until all weak pointers are also ‘dead’... let's see why

Weak pointer and shared pointer

When I was working on the Smart Pointer Reference Card, I ran into quite an interesting issue. It appears that in some cases, memory allocated for the object controlled by smart_ptr might not be released until all weak pointers are also ‘dead’.

Such a case was surprising to me because I thought that the moment the last share_ptr goes down, the memory is released.

Let’s drill down the case. It might be interesting as we’ll learn how shared/weak pointers interact with each other.

One note: This article comes from my blog @bfilipek.com - How a weak_ptr might prevent full memory cleanup of managed object.

The Case

Ok, so what’s the problem?

First, we need to see the elements of the case:

  • a managed object, let’s assume it’s big (as sizeof would report)
    • Object might contain other pointers/containers that can allocate own memory chunks, so they don't contribute much to the final object size (apart from a few pointers). For example, std::vector uses allocates memory for the stored elements.
  • shared_ptr (one or more) - they point to the above object (resource)
  • make_shared - used to create a shared pointer
  • weak_ptr
  • the control block of shared/weak pointers

The code is simple:

Shared pointers to our large object go out of the scope. The reference counter reaches 0, and the object can be destroyed. But there’s also one weak pointer that outlives shared pointers.

C++
weak_ptr<MyLargeType> weakPtr;
{
    auto sharedPtr = make_shared<MyLargeType>();
    weakPtr = sharedPtr;
    // ...
}
cout << "scope end...\n";

In the above code, we have two scopes: inner - where the shared pointer is used, and outer - with a weak pointer (notice that this weak pointer holds only a ‘weak’ reference, it doesn’t use lock() to create a strong reference).

When the shared pointer goes out the scope of the inner block, it should destroy the managed object… right?

This is important: when the last shared pointer is gone, this destroys the objects in the sense of calling the destructor of MyLargeType (this will also release memory for members that allocate their separate memory chunks, like vectors, strings, etc)… but what about the allocated memory for the object? Can we also release it?

To answer that question, let’s consider the second example:

C++
weak_ptr<MyLargeType> weakPtr;
{
    shared_ptr<MyLargeType> sharedPtr(new MyLargeType());
    weakPtr = sharedPtr;
    // ...
}
cout << "scope end...\n";

Almost the same code… right? The difference is only in the approach to create the shared pointer: here we use explicit new.

Let’s see the output when we run both of those examples.

In order to have some useful messages, I needed to override global new and delete, plus report when the destructor of my example class is called.

C++
void* operator new(size_t count) {
    cout << "allocating " << count << " bytes\n";
    return malloc(count);
}

void operator delete(void* ptr) noexcept {
    cout << "global op delete called\n";
    free(ptr);
}

struct MyLargeType {
    ~MyLargeType() { cout << "destructor MyLargeType\n"; }

private:
    int arr[100]; // wow... so large!!!!!!
};

Ok, ok… let’s now see the output:

For make_shared:

allocating 416 bytes
destructor MyLargeType
scope end...
global op delete called

and for the explicit new case:

C++
allocating 400 bytes
allocating 24 bytes
destructor MyLargeType
global op delete called
scope end...
global op delete called

What happens here?

The first important observation is that, as you might already know, make_shared will perform just one memory allocation. With the explicit new, we have two separate allocations.

So we need a space for two things: the object and... the control block.

The control block is implementation dependant, but it holds the pointer to an object and also the reference counter. Plus some other things (like custom deleter, allocator, …).

When we use explicit new, we have two separate blocks of memory. So when the last shared pointer is gone, then we can destroy the object and also release the memory.

So we see the output:

C++
destructor MyLargeType
global op delete called

Both the destructor and free() is called - before the scope ends.

However, when a shared pointers is created using make_shared(), then the managed object resides in the same memory block as the rest of the implementation details.

Here’s a picture with that idea:

Control block of shared pointers

The thing is that when you create a weak pointer, then inside the control block "weak counter" is usually increased. Weak pointers and shared pointers need that mechanism so that they can answer the question “is the pointer dead or not yet”, (or to call expire() method).

In other words, we cannot remove the control block if there’s a weak pointer around (while all shared pointers are dead). So if the managed object is allocated in the same memory chunk, we cannot release memory for it as well - we cannot free just part of the memory block (at least not that easily).

Below, you can find some code from MSVC implementation, this code is called from the destructor of shared_ptr (when it’s created from make_shared):

C++
~shared_ptr() _NOEXCEPT
{   // release resource
    this->_Decref();
}

void _Decref()
{    // decrement use count
    if (_MT_DECR(_Uses) == 0)
    {    // destroy managed resource,
        // decrement weak reference count
        _Destroy();
        _Decwref();
    }
}

void _Decwref()
{    // decrement weak reference count
    if (_MT_DECR(_Weaks) == 0)
    {
        _Delete_this();
    }
}

As you see there’s separation of destroying the object - that only calls destructor, and Delete_this() - only occurs when the weak count is zero.

Here's the link if you want to play with the code: Coliru example.

Fear Not!

The story of memory allocations and clean up is interesting… but does it affect us that much?

Possibly not much.

You shouldn’t stop using make_shared just because of that reason! :)

The thing is that it’s quite a rare situation.

Still, it’s good to know this behaviour and keep it in mind when implementing some complex systems that rely on shared and weak pointers.

For example, I am thinking about the concurrent weak dictionary data structure presented by Herb Sutter: My Favorite C++ 10-Liner | GoingNative 2013 | Channel 9.

Correct me if I’m wrong:

make_shared will allocate one block of memory for the control block and for the widget. So when all shared pointers are dead, the weak pointer will live in the cache… and that will also cause the whole memory chunk to be there as well. (Destructors are called, but memory cannot be released).

To enhance the solution, there should be some additional mechanism implemented that would clean unused weak pointers from time to time.

Remarks

After I understood the case, I also realized that I’m a bit late with the explanation - others have done it in the past. :) Still, I’d like to note things down.

So here are some links to resources that also described the problem:

From Effective Modern C++, page 144:

As long as std::weak_ptrs refer to a control block (i.e., the weak count is greater than zero), that control block must continue to exist. And as long as a control block exists, the memory containing it must remain allocated. The memory allocated by a std::shared_ptr make function, then, can’t be deallocated until the last std::shared_ptr and the last std::weak_ptr referring to it have been destroyed.

Summary

The whole article was a fascinating investigation to do!

Sometimes, I catch myself spending too much time on things that maybe are not super crucial. Still, they are engaging. It’s great that I can share this as an article. :)

The bottom line for the whole investigation is that the implementation of shared and weak pointers is quite complex. When the control block is allocated in the same memory chunk as the managed object, a special care has to be taken when we want to release the allocated memory.

Remember that such behaviour of shared_ptr/make_shared is not a major "flaw" and you should be still using them! Just be aware that if you have some weak_ptr around and you really care about memory, then special care needs to be taken.

BTW: once again, here's the link to C++ Smart Pointers Reference Card, you can download it if you like.

This article was originally posted at http://www.bfilipek.com/2017/12/weakptr-memory.html

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Poland Poland
Software developer interested in creating great code and passionate about teaching.

Author of C++17 In Detail - a book that will teach you the latest features of C++17!

I have around 11 years of professional experience in C++/Windows/Visual Studio programming. Plus other technologies like: OpenGL, game development, performance optimization.

In 2018 I was awarded by Microsoft as MVP, Developer Technologies.

If you like my articles please subscribe to my weekly C++ blog or just visit www.bfilipek.com.

Comments and Discussions

 
QuestionDoes it matter? Hell, Yes! Pin
john morrison leon18-Feb-18 0:40
john morrison leon18-Feb-18 0:40 
So if you use make_shared then any weak_ptrs to that object will invisibly become shared owners.

Does it matter? Hell, Yes!
Here is some example code I wrote, a bare bones shared object server:

C++
shared_ptr<FontFace> getFontFace()
{
	static weak_ptr<FontFace> wpFontFace;
	
	shared_ptr<FontFace> spFontFace = wpFontFace.lock();
	if (nullptr == spFontFace)
	{
		wpFontFace = spFontFace = std::make_shared<FontFace>();
	}
		
	return  spFontFace;
}

I used make_shared() in good faith, it comes from the Standard Library and it looks neat. The problem is that the server holds only a weak_ptr to the object for good reason. It is so that the server itself doesn't keep the object alive when nobody else is using it anymore. Well I now know (Ouch!) , thanks to your article, that make_shared breaks weak_ptr and the whole edifice will have exactly the same object dynamics as:

C++
FontFace& getFontFace()
{
	static FontFace face; //created first time the function is called
	return  face;	//return reference to eternal object;
}


Here is the correction I have applied:

C++
shared_ptr<FontFace> getFontFace()
{
	static weak_ptr<FontFace> wpFontFace;
	
	shared_ptr<FontFace> spFontFace = wpFontFace.lock();
	if (nullptr == spFontFace)
	{
		wpFontFace = spFontFace = std::shared_ptr<FontFace>(new FontFace);
	}
		
	return  spFontFace;
}


The problem now is how do I stop people from 'correcting' the 'old fashioned' code with make_shared() and breaking it again.

Binding together the object and a control block that contains a weak_count in the same allocation is a logical absurdity. It is a hack that you shouldn't even be thinking about unless you have a demanding high performance scenario that requires no weak_ptr observation. It should have been called make_shared_break_weak_hack() so that we can be aware of the trade off.

A vote of 5 for diligent observation and the courage to challenge received wisdom.

I have just realised that the broken weak_ptr doesn't keep the object usefully alive because its destructor does get called, it just stops its memory from being released. For this reason the make_shared() version of my server example will execute as expected. Nevertheless it will always retain a dead FontFace object whenever there is no demand. That is messy and was not my design intention.

modified 18-Feb-18 7:57am.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.