22

If I call std::make_shared<T> (rather than just allocating a shared_ptr<T> explicitly) then I expect the reference count to be allocated in memory alongside the instance of T, for performance reasons. All well and good.

But if I have weak_ptr instances referencing the same object, presumably they will need access to that reference count, to know whether the object still exists.

So, when the last shared_ptr to the instance of T is destroyed, a naive understanding of the system would imply that it cannot deallocate the memory that T is stored in, because weak_ptrs still require access to that count.

It seems like there is a separate weak reference counter and in theory that could be held separately from the instance of T, so that the T can be destroyed and the memory deallocated while weak references still exist. But then we're back to having 2 separate allocations, thwarting the benefits of make_shared.

I assume I am misunderstanding something here. How can the memory allocated for a instance constructed via std::make_shared be freed when weak references exist?

Community
  • 1
  • 1
Kylotan
  • 18,290
  • 7
  • 46
  • 74

2 Answers2

13

If you use make_shared and if the implementation uses a single allocation for both the object and the reference counts, then that allocation cannot be freed until all references (both strong and weak) have been released.

However, the object will be destroyed after all strong references have been released (regardless of whether there are still weak references).

James McNellis
  • 348,265
  • 75
  • 913
  • 977
  • So, to make sure I understand this - make_shared buys some performance at allocation time at the cost of deallocation being delayed, possibly until the end of the process (if a weak_ptr is not destroyed or reset)? If so, couldn't this be considered a memory leak of sorts? (The objects may be managed correctly, but the memory itself is not.) – Kylotan Nov 23 '12 at 21:26
  • 2
    @Kylotan: Not by any definition of "memory leak" that I've ever heard. A memory leak is when memory remains allocated which you no longer have access to, because you've lost all references to it. But if you lose the last weak_ptr, the memory is reclaimed, it is no longer allocated. – Benjamin Lindley Nov 23 '12 at 21:30
  • 3
    In theory, a free store could have the ability to accept a "partial return" of memory. Sort of like shrinking `reaclloc`. In practice? Probably not. – Yakk - Adam Nevraumont Nov 23 '12 at 21:30
  • 6
    Doesn't this kind of defeat the purpose of weak pointers though. The crux of a weak pointer, compared to a strong pointer, is that the strong pointer delays the release of the object. Keeping a weak pointer to an object is supposed to be very cheap, but if it can make the entire object stick in memory, it's quite expensive. – David Schwartz Nov 23 '12 at 21:39
  • 1
    That's my concern, certainly. I concede that calling it a memory leak is not technically true but I would also argue that a weak_ptr is no longer really a weak reference in this context. The object lifetime is correctly managed but memory is an important resource also, and one reason (among many) for using C++ is to have a close correlation between object destruction and memory deallocation. – Kylotan Nov 23 '12 at 21:46
  • 3
    There are two reasons it's not usually as big a deal as people might think. First, for most large objects, their size is not due to their initial allocation but things later attached to them. (For example, in a map, list or vector). These will be freed by the invocation of the object's destructor, so they won't hang around. Second, many weak pointer implementations expect the weak pointer to avoid delaying the destruction of the object *indefinitely*. So long as the weak pointer itself is destroyed relatively soon after the object goes away, the extra memory usage will be brief. – David Schwartz Nov 23 '12 at 23:45
  • 5
    Most importantly of all... you could simply not use `make_shared`. The point of `make_shared` (in terms of optimization) is that you're making a choice. You want a single allocation instead of two. That choice has *consequences*: if you have weak references, then the single allocation won't get cleaned up until all weak references are gone. If you don't want that consequence, then don't use `make_shared`. – Nicol Bolas Nov 24 '12 at 00:07
  • 2
    @Kylotan: "memory is an important resource also, and one reason (among many) for using C++ is to have a close correlation between object destruction and memory deallocation." I disagree; the main point is *determinism*: you know when which operations happen and you have control over them. In a GC system, there is no "object destruction" (outside of finalizers, which are generally to be avoided). The GC will delete things whenever it feels like. In C++, you know when objects are constructed/destroyed, and when memory is allocated/released. You still have that control with `weak_ptr`s. – Nicol Bolas Nov 24 '12 at 00:10
  • 1
    @NicolBolas: the whole point of this question was to determine the consequences - not one piece of documentation that I could find (except here, now) has stated that weak_ptrs will keep the memory around even once all shared_ptr references are gone - in fact the implication is otherwise. As for determinism, that's just a different way of looking at the same thing. The owner of an object can't necessarily access the weak_ptrs that reference it so it makes it very hard to know exactly when the memory will be freed. – Kylotan Nov 24 '12 at 00:28
  • @Kylotan: If it's stored in a `shared_ptr`, then there is no "the owner of an object". That's rather the point. Also, determinism doesn't mean *trivial* determinism; that's also the point of `shared_ptr`. The destruction of a `shared_ptr` *might* cause the destruction of the object, but it might not. Whether it does or not depends on other program state. – Nicol Bolas Nov 24 '12 at 00:38
  • @NicolBolas: The shared_ptrs share the ownership of the object. The implication is that when all the shared_ptrs are gone, so is the object. And it is indeed destroyed, but the memory is not necessarily deallocated. And that is a potential problem, given that it works differently to the usual ref-counted memory management, and that it isn't clearly documented anywhere. – Kylotan Nov 24 '12 at 00:41
  • 4
    I agree that documentation is the central problem here. It's fine that make_shared results in memory hanging around a bit longer, but this needs be carefully spelled out. Otherwise, people will not understand the trade-offs they are making. – Peter Ruderman Jan 03 '13 at 19:33
3

The common implementation is for the ref control block of an std::shared_ptr to contain both a strong and a weak reference count separately. The managed object is destroyed when the strong reference count goes to zero, but the ref control block itself is only released when the weak reference count also reaches zero.

(When you use std::make_shared, the ref control block itself contains enough memory to hold the managed object. This is just a detail.)

In other words, the observable behaviour for the managed object is independent of weak pointers.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • Does that apply when `make_shared` is used? I was under the impression that the typical implementation (perhaps not mandated by the standard) is to allocate the ref control block and the object itself contiguously. – Kylotan Nov 23 '12 at 21:58
  • @Kylotan: Yes, those details have no relevance. The managed object will be in-place constructed and destroyed in the larger memory block. – Kerrek SB Nov 23 '12 at 22:31
  • Maybe I wasn't clear, but I'm not actually interested in the object's behaviour, but the lifetime of the allocated memory it lives in. The answer above implies this is dependent on weak refs in the make_shared case and not dependent on weak refs if I create a shared_ptr explicitly. – Kylotan Nov 23 '12 at 22:50
  • @Kylotan: The memory lives until all the weak pointers are gone, but the managed object only lives until all the strong references are gone. Is that not clear? I can spell out a code example if you like. – Kerrek SB Nov 23 '12 at 22:52
  • That's clear - I was just unsure if you were contradicting the other answer or not. – Kylotan Nov 23 '12 at 22:55