Some time ago, we peeked inside the atomic shared_ptr to see how it worked. Can we apply these same principles to create an atomic com_ptr
?
Recall that the atomic shared_ptr operates by using the bottom bit of the control block pointer as a lock flag, so that nobody can change the value while we’re copying the pointer and incrementing the reference count. Can we do this with a com_ptr
?
We could use the same trick of using the bottom bit of the raw COM pointer as a lock flag. This is acceptable because COM pointers must be pointer-aligned (since they point to a vtable), so we know that the bottom bit of a valid COM pointer is clear. However, we run into trouble when trying to increment the reference count: The call to IUnknown::
happens while the lock is held, but the AddRef
is a call out to external code, and we don’t know what it’s going to do. We know what it’s supposed to do (namely, increment the reference count), but it may take a circuitous route to get there, including passing through aggregated controlling unknowns, tear-off stubs, tear-offs of aggregated objects, weak outer pointers, and other fanciful characters.
We know that holding a lock while calling out to external code is a source of deadlocks, so holding a lock while calling out to a mystery implementation of IUnknown::
is probably not a good idea.
Sorry.
Author of `atomic::wait` and `atomic<shared_ptr<T>>::wait` PRs here.
I'm afraid that `atomic<T>::wait` is problematic too, but for a different reason.
Turns out that `atomic<shared_ptr<T>>::wait` should also treat the value as changed if the value pointer is not changed, but the control block is different. This could have been achieved with `WaitOnAddress` on two pointers residing contiguously in memory, but the maximum size for `WaitOnAddress` is 8 bytes. Another contributor fixed that by adding timed backoff in his PR.
I've created an issue to revisit that in the future. But unless `WaitOnAddress` with 16 bytes emerges, any other solution, like using "indirect" wait (what the...
@Raymond Chen
Sorry for the offtopic Raymond, but I want to let you know that:
- Turning off email notifications in this site profile doesn't work (I still get them)
- This commenting system is broken:
post A -> post B (reply to A) -> post C (reply to B) <-- you are here and you can't reply to it -- why not just drop the pretense of supporting threaded conversations and allow for post quoting?
- Sign in (with Microsoft at least) doesn't keep you signed in and you have to sign in every time you visit...
I’ve asked the site maintainers to look into it.
Calling
AddRef
is plain sailing compared to callingRelease
, which can run destructors.I don't think this is likely to be an issue, at least for atomic std::shared_ptr, because C++ specifies that the destruction of the underlying object is sequenced after the atomic operations on the refcount. Effectively, your weak pointers all see the object as "already gone" even if its destructor has not yet run (and C++ specifies that behavior even for non-atomic std::weak_ptr). So you don't need to hold a lock when you run the destructor, because by the time you're destroying the pointee, you've already prevented anyone else from getting a pointer to it, and you are explicitly not required...
Update (2025-04-29): The following argument was wrong as pointed out in Mike Winterberg's reply. But if you don't specialize , then the default implementation uses a lock anyway, so you have exactly the same risk of deadlock, right? So what's the benefit of declining to provide the specialization (and the resulting small performance improvement)? Is the existence of a specialization going to make a difference in whether users realize there is a deadlock risk unless they incorporate the hidden lock and into their lock hierarchy? It may be worth noting that while this article apparently...
Keeping in mind your disclaimer… std::atomic<com_ptr> won’t compile, since the primary std::atomic template requires that the object it contains to be trivially copyable. Since com_ptr defines a copy constructor, it isn’t.
You are right. My knowledge of the primary template for
std::atomic<T>
was based on another article on this blog, and I neglected to look up whether there were restrictions onT
. That invalidates my first point, so I crossed it out. Thanks.std::atomic<shareable_com_ptr<T>> does not behave like std::atomic<com_ptr<T>> since std::atomic::store is noexcept. having the lower bits of the pointer act as a counter of outstanding or excess add_ref calls would work. with the only problem being a deadlock if the add_ref call calls std::atomic<com_ptr<T>>::load on its owner. a full split refcount using 128bit CAS avoids this
is another issue I didn't research; thanks for pointing it out. To confirm, what potential exception(s) from are you concerned about? Failure to allocate heap memory for the new ? From what I read, COM methods aren't supposed to throw C++ exceptions, so calling in a function might technically be OK, though I'm starting to think it's inconsistent with the spirit of .
Let's put aside for now the question of whether it's reasonable to expose the solution as a specialization and suppose our goal is just to provide a thread-safe wrapper...
My take is that std::atomic<com_ptr> is also a mistake for the same reason. Most usages of COM pointers are part of larger operations, so you already need a lock and also can assess the dangers of calling into T while holding that lock. But I don’t like std::atomic<com_ptr> making that risk assessment for me.