Avoiding double-destruction when an object is released

Date:September 28, 2005 / year-entry #282
Tags:code
Orig Link:https://blogs.msdn.microsoft.com/oldnewthing/20050928-10/?p=34013
Comments:    23
Summary:As we saw last time, trying to do too much in one's destructor can lead to an object being destroyed twice. The standard way to work around this problem is to set an artificial reference count during destruction. class MyObject : public IUnknown { ... ULONG Release() { LONG cRef = InterlockedDecrement(&m_cRef); if (cRef ==...

As we saw last time, trying to do too much in one's destructor can lead to an object being destroyed twice. The standard way to work around this problem is to set an artificial reference count during destruction.

class MyObject : public IUnknown
{
 ...
 ULONG Release()
 {
  LONG cRef = InterlockedDecrement(&m_cRef);
  if (cRef == 0) {
   m_cRef = DESTRUCTOR_REFCOUNT;
   delete this;
  }
  return cRef;
 }
 ...
private:
 }
 enum { DESTRUCTOR_REFCOUNT = 42 };
 ~MyObject()
 {
  if (m_fNeedSave) Save();
  assert(m_cRef == DESTRUCTOR_REFCOUNT);
 }
};

If you have a common implementation of IUnknown, you can set the reference count to DESTRUCTOR_REFCOUNT in your implementation of IUnknown::Release like we did here, and assert that the value is correct in your implementation's destructor. Since C++ runs base class destructors after derived class destructors, your base class destructor will check the reference count after the derived class has done its cleanup.

By setting the reference count to an artificial non-zero value, any AddRef() and Release() calls that occur will not trigger a duplicate destruction (assuming of course that nobody in the destructor path has a bug that causes them to over-release). The assertion at the end ensures that no new references to the object have been created during destruction.

This is really more of a workaround than a rock-solid solution, because it assumes that no functions called during the destruction sequence retain a reference to the object beyond the function's return. This is in general not something you can assume about COM. In general, a method is free to call AddRef and hang onto a pointer to an object in order to complete the requested operation later. Some methods (such as the IPersistPropertyBag::Load method) explicitly forbid such behavior, but these types of methods are more the exception rather than the rule.

Exercise: Why is it safe to perform a simple assignment m_cRef = DESTRUCTOR_REFCOUNT instead of the more complicated InterlockedExchangeAdd(&m_cRef, DESTRUCTOR_REFCOUNT)?


Comments (23)
  1. Tim Smith says:

    As someone pointed out, you might have vtable problems with this solution. But you so say it is a workaround and not a full solution.

    Simple assignment works since all references to the object are gone and thus there shouldn’t be any threading issues.

  2. Graham Harper says:

    This "m_cRef = DESTRUCTOR_REFCOUNT" is safe simply because if your executing this code nothing else has a reference to the object (in any thread). Only one thread can "Clean up".

    If any other thread did have a reference the count wouldn’t have transitioned to zero.

    The only caveat is when someone has got a non-referenced counted pointer to the object (intentionally or unintentionally). If this is the case then they deserve everything they get (crashes, corrupt memory, objects disappearing etc)

  3. Jack Mathews says:

    It’s 2005, no need for the enum hack anymore :)

  4. Arlie Davis says:

    Um, I would strongly chastise any developer on my team who wrote such code. A reference count of 42 is quite low; when you include callback interfaces, "site" interfaces, etc. 42 is well within the bounds for a reasonable program.

    If you must, use a negative value, or at least some huge positive value.

    My solution to this has been to mandate that object destructors only deal with freeing memory (breaking reference cycles on subgraphs, etc.) or releasing handles to unmanaged resources. "Semantic" actions, such as saving a file, are forbidden.

  5. Zahical says:

    "Bytes 2-3 The second word of the file is the TIFF version

    number.
    This number, 42 (2A in hex), is not to be equated

    with the current Revision of the TIFF specification….

    …The number 42 was chosen for its deep

    philosophical significance."

  6. Phaeron says:

    Doing the Save() operation from Release() instead of from the destructor would avoid the vtable problem and allow the object to survive past the "last" Release() call, since you can keep a legitimate reference on the object. However, you can still run into nasty problems with a client function that isn’t reentrant. Often member pointers are released in "mp->Release(); mp = NULL;" order, so if you do something in your Release() that causes recursion into the client function, you’ll hit a double-Release() problem.

  7. I agree with Arlie in that destructors need to be limited to memory housekeeping and not user actions. If the user wants to save/write the settings – let them explicitly do that.

    I wouldn’t multipurpose your m_cRef. That’s for certain. What you really need is a simple state variable ("bool m_bShuttingDown"?) or an enum (if you want to get detailed) to check against before doing potentially crashing activities.

  8. @Arlie –

    The 42 is not a reference count threshold, it’s just being used as a flag. I suppose Raymond used it because 42 is the answer to all questions =)

    Maybe using -1 would work better and be more readable.

  9. kbiel says:

    Arlie,

    Using 42 is completely safe since the assertion will not occur until the ref count has reached zero and the object is being destroyed. At this point any flag value is valid except for 0.

    Mandating that destructors only deal with freeing memory and handles and breaking reference cycles is short-sighted. If that is true, then your object must rely on the consumer to perform some critical behavior such as saving before releasing your object which is even worse than the work around presented in this article since no consumer should be required to keep track of your references.

  10. kbiel says:

    Klaus,

    42 is certainly not the answer to ALL questions. It is only the answer to The Ultimate Question of Life, The Universe, and Everything. Now if only we knew the question. :)

  11. Daev says:

    Silly question, I’m sure, but why is InterlockedDecrement used?

    The only reason I can think of is for multithread safety. But that Release function doesn’t look multithread safe — what if another thread was about to increment m_cRef? Does the AddRef refcount incrementer have a special interlocked check for zero to catch this case?

    (Mind you, I don’t know anything about the COM system, I’m just looking at it from the perspective of refcounting.)

  12. PiersH says:

    of course, if you were using ATL (and implementinf FinalRelease() ) then this would never have been a problem.

    // Set refcount to -(LONG_MAX/2) to protect destruction and

    // also catch mismatched Release in debug builds

    virtual ~CComObject() throw()

    {

    m_dwRef = -(LONG_MAX/2);

    FinalRelease();

    #ifdef _ATL_DEBUG_INTERFACES

    _AtlDebugInterfacesModule.DeleteNonAddRefThunk(_GetRawUnknown());

    #endif

    _pAtlModule->Unlock();

    }

  13. Daev: Think about how it would be possible for somebody to AddRef() while somebody else was Release()ing to zero.

  14. Chris Becke says:

    @kbiel: MAndating that destructiosn deal with trivial things is entirely reasonable. As the ::Release method can deal with what happens when the ref-count hits zero before it destroys the object.

  15. Maks Verver says:

    @Arlie: the ‘flag’ value must either be positive or (if negative) very large. A small positive value (eg. 1) is preferred to avoid wrap-around. A small negative value doesn’t work, because a series of increments and decrements could result in another deletion!

    Note that the flag value is a lower bound on the possible values the reference count can take (assuming the application increments and decrements correctly – otherwise things go wrong anyway), so 42 is actually fine as a flag value, but definitely -1 or 2147483647 would definitely not be.

  16. Norman Diamond says:

    Wednesday, September 28, 2005 3:17 PM by oldnewthing

    > Daev: Think about how it would be possible

    > for somebody to AddRef() while somebody else

    > was Release()ing to zero.

    I’m not Daev but I’m trying to think about this. It seems to me that the reason why InterlockedDecrement() is needed is: it would be possible for somebody to AddRef() while somebody else was Release()ing to NONZERO. If somebody else was releasing to zero then no one has a valid pointer to this object for the purpose of AddRef(), so maybe the Release() method could try to figure out whether or not it needs to call InterlockedDecrement() at this particular time or not, but there’s no point in trying to figure that out.

  17. Graham Harper says:

    One reason the Interlocked functions are needed can been be seen from the disassembly of this simple code:

    –m_lRef;

    When disassembling the simple prefix decrement above from an executable generated by the VS2002 (x86) C++ compiler you get (something like) the following (assume eax holds the address of m_lRef):

    mov ecx,dword ptr [eax]

    sub ecx,1

    mov dword ptr [eax],ecx

    If the kernel (for what ever reason) preempts this thread between the first MOV and SUB instruction and subsequently schedules another thread that completes an AddRef() in it’s entirety, the code above (when scheduled again by the kernel) will be operating on a “stale” value.

    This can result in all sorts of strange behavior in a multi-threaded environment. Objects disappearing when threads have “valid” references is just one symptom.

    Looking at the disassembly of the “InterlockedDecrement” function (again on an x86 machine) from Kernel32 you can see that the load, decrement and store is done atomically thus alleviating the errors encountered above:

    SYM:_InterlockedDecrement@4

    0x7C809794: 8B4C2404 MOV ECX,DWORD PTR [ESP+0x4]

    0x7C809798: B8FFFFFFFF MOV EAX,0xFFFFFFFF

    0x7C80979D: F00FC101 LOCK XADD DWORD PTR [ECX],EAX

    0x7C8097A1: 48 DEC EAX

    0x7C8097A2: C20400 RET 0x4

    Interestingly the COM specification does not guarantee the accuracy of the reference count returned by AddRef() or Release() UNLESS the return value is 0. It’s easy to understand why given the fact that you cannot accurately determine the value of the ref count at any-one point unless you are the thread cleaning up, in which case the object is no longer in a volatile state. (NOTE: This is not the only reason COM doesn’t guarantee these return values)

  18. Moasat says:

    The Interlocked functions should be used for both AddRef and Release if the code will be used in a multi-threaded project. Even though you only have to know if they hit zero, it still needs to be thread-safe. This is assuming multiple threads can and will be calling AddRef and Release.

    The reason you can do a simple assign to cRef is because it is simply that, an assign. An assign is not a read/modify/write operation. It is already atomic. Increment/Decrement are not atomic and so must have a LOCK prefix, or in Windows terms an Interlocked prefix.

  19. kbiel says:

    Chris: MAndating that destructiosn deal with trivial things is entirely reasonable. As the ::Release method can deal with what happens when the ref-count hits zero before it destroys the object.

    You still have to deal with the exact same issues whether you call your save method from ::Release or from the destructor. Beside that fact, not everything is COM.

  20. ipoverscsi says:

    @Daev

    Once the Class Object (essentially a factory for creating COM objects) creates an instance of a COM object, the only way to get another reference to that object is if you already have a reference to it. This means that it is impossible for AddRef() to be called (in a valid program) if the InterlockedDecrement() in Release() results in the reference count being zero.

  21. Daev says:

    Some very insightful comments have been made showing why the Interlocked instructions are required in a multithreaded program.

    However, my original question was a little different: not why is the InterlockedDecrement necessary, but why is it *sufficient*?

    (Raymond’s answer was a very diplomatic way of suggesting I think about how COM works a bit more. I’ve been reasoning from a naive model of refcounted pointers to structs, since I don’t know COM.)

    Assuming AddRef is nothing but an InterlockedIncrement instruction, why doesn’t the entire Release function need to be mutex-protected? What if one thread is about to call InterlockedIncrement in AddRef when a second thread gets control, ends up decrementing the object’s refcount to zero, and is then about to call "delete" when the first thread regains control?

Comments are closed.


*DISCLAIMER: I DO NOT OWN THIS CONTENT. If you are the owner and would like it removed, please contact me. The content herein is an archived reproduction of entries from Raymond Chen's "Old New Thing" Blog (most recent link is here). It may have slight formatting modifications for consistency and to improve readability.

WHY DID I DUPLICATE THIS CONTENT HERE? Let me first say this site has never had anything to sell and has never shown ads of any kind. I have nothing monetarily to gain by duplicating content here. Because I had made my own local copy of this content throughout the years, for ease of using tools like grep, I decided to put it online after I discovered some of the original content previously and publicly available, had disappeared approximately early to mid 2019. At the same time, I present the content in an easily accessible theme-agnostic way.

The information provided by Raymond's blog is, for all practical purposes, more authoritative on Windows Development than Microsoft's own MSDN documentation and should be considered supplemental reading to that documentation. The wealth of missing details provided by this blog that Microsoft could not or did not document about Windows over the years is vital enough, many would agree an online "backup" of these details is a necessary endeavor. Specifics include:

<-- Back to Old New Thing Archive Index