We continue our survey of duck-typing requirements of various C++ COM smart pointer libraries by looking at ATL’s CComPtr
, running it through our standard tests.
// Dummy implementations of AddRef and Release for // testing purposes only. In real code, they would // manage the object reference count. struct Test { void AddRef() {} void Release() {} Test* AddressOf() { return this; } }; struct Other { void AddRef() {} void Release() {} }; // Pull in the smart pointer library // (this changes based on library) #include <atlbase.h> #include <atlcom.h>Â Â using TestPtr = CComPtr<Test>;Â Â using OtherPtr = CComPtr<Other>; void test() { Test test; // Default construction TestPtr ptr; // Construction from raw pointer TestPtr ptr2(&test); // Copy construction TestPtr ptr3(ptr2); // Attaching and detaching auto p = ptr3.Detach(); ptr.Attach(p); // Assignment from same-type raw pointer ptr3 = &test; // Assignment from same-type smart pointer ptr3 = ptr; // Accessing the wrapped object // (this changes based on library) if (ptr.p != &test) { std::terminate(); // oops } if (ptr->AddressOf() != &test) { std::terminate(); // oops } // Returning to empty state ptr3 = nullptr; // Receiving a new pointer // (this changes based on library) Test** out = &ptr3; out = &ptr3.p; // Bonus: Comparison. if (ptr == ptr2) {} if (ptr != ptr2) {} if (ptr < ptr2) {} // Litmus test: Accidentally bypassing the wrapper ptr->AddRef(); ptr->Release(); // Litmus test: Construction from other-type raw pointer Other other; TestPtr ptr4(&other); // Litmus test: Construction from other-type smart pointer OtherPtr optr; TestPtr ptr5(optr); // Litmus test: Assignment from other-type raw pointer ptr = &other; // Litmus test: Assignment from other-type smart pointer ptr = optr; // Destruction }
A glitch in the core functionality tests happens when we call ptr3.Attach(p)
:
atlcomcli.h(250,1): error C2440: 'initializing': cannot convert from 'void' to 'ULONG'
The problem is here:
// Attach to an existing interface (does not AddRef) void Attach(_In_opt_ T* p2) throw() { if (p) { ULONG ref = p->Release(); (ref); // Attaching to the same object only works if duplicate // references are being coalesced. Otherwise // re-attaching will cause the pointer to be released and // may cause a crash on a subsequent dereference. ATLASSERT(ref != 0 || p2 != p); } p = p2; }
ATL expects the Release
method to return a ULONG
representing the new reference count. So let’s fix our class to do that.
struct Test { void AddRef() { } // Dummy implementation for testing purposes only. ULONG Release() { return 1; } };
Okay, that gets us past the Attach
/Detach
test.
There are two ways to receive a new pointer. You can use the &
operator directly on the CComPtr
, which asserts that the smart pointer is already empty. If the pointer is an in/out parameter, then you can take the address of the public p
member directly, which avoids the assertion check. There is no combined method for “release previous pointer before receiving a new one”.
The comparison tests work as expected. They just compare the wrapped pointers.
The accidental bypass litmus test and the test for accessing the wrapped object via the ->
operator are interesting because CComPtr
uses a techique that author Jim Springfield called “coloring”:
template <class T> class _NoAddRefReleaseOnCComPtr : public T { private: STDMETHOD_(ULONG, AddRef)()=0; STDMETHOD_(ULONG, Release)()=0; }; template <class T> class CComPtrBase { ... _NoAddRefReleaseOnCComPtr<T>* operator->() const throw() { ATLASSERT(p!=NULL); return (_NoAddRefReleaseOnCComPtr<T>*)p; } ... };
The trick here is that instead of returning the wrapped T*
directly, we pretend that it is a pointer to the T
portion of a _NoAddRefReleaseOnCComPtr<T>
, and return a pointer to that derived class. The _NoAddRefReleaseOnCComPtr<T>
class declares the AddRef
and Release
as private, thereby making them inaccessible from the resulting ->
operator:
ptr->Release(); // error: Cannot call private method¹
In the case where T
derives from IUnknown
, these virtual AddRef
and Release
methods override the same-signature methods in IUnknown
. But in the case where T
does not derive from IUnknown
, this adds a vtable to _NoAddRefReleaseOnCComPtr
. Now, this vtable is never materialized, but it nevertheless introduces some pointer arithmetic that the compiler cannot immediately optimize away, because a static_cast
is not always just a pointer adjustment.
; ideally CComPtr<Test>::operator->() lea rax, [rcx-8] ret ; actually CComPtr<Test>::operator->() lea rdx, [rcx-8] neg rcx sbb rax, rax and rax, rdx ret
The extra nonsense is there so that the cast from (T*)
to (_NoAddRefReleaseOnCComPtr<T>*)
produces nullptr
when passed nullptr
. (The “ideal” version would return -8
.)
I call this a missed optimization because when the compiler inlines the ->
operator, it can see that the resulting pointer is immediately dereferenced, so it cannot be null. Furthermore, the +8
that comes afterward to convert the (_NoAddRefReleaseOnCComPtr<T>*)
back to a (T*)
exactly cancels out the -8
, so all the pointer nonsense can be optimized out entirely. Bonusly furthermore, this
can never be nullptr
; invoking a method on a null pointer is undefined behavior.
; ptr2->AddressOf() ; ideally mov rcx, [ptr2].p call Test::AddressOf ; actually mov rcx, [ptr2].p mov eax, 8 test rcx, rcx cmove rcx, rax call Test::AddressOf
All of these problems could have been avoided if _NoAddRefReleaseOnCComPtr
had declared the private AddRef
and Release
as non-virtual.
template <class T> class _NoAddRefReleaseOnCComPtr : public T { private: ULONG AddRef(); ULONG Release(); };
This still accomplishes the task of making the AddRef
and Release
methods inaccessible, but it doesn’t introduce a vtable, which means that the static_cast
operations do not result in any code generation.
But it’s too late to fix that now. That would be a binary breaking change.
Other consequences of “coloring” are that the wrapped class T
cannot be final
, and if the T::AddRef
and T::Release
methods are virtual, they must return ULONG
and use STDMETHODCALLTYPE
.
The CComPtr
passes the other litmus tests: Most of the operations generate an error complaining that the types do not match. The interesting one is the last one, the assignment of an other-type smart pointer.
template <typename Q> T* operator=(_Inout_ const CComPtr<Q>& lp) throw() { if(!this->IsEqualObject(lp) ) { AtlComQIPtrAssign2((IUnknown**)&this->p, lp, __uuidof(T)); } return *this; }
This one fails because the code uses CComPtrBase::IsEqualObject
, which in turn does not compile due to lack of QueryÂInterface
support. Which is a good thing, because there is a C-style cast to IUnknown**
in the call to AtlComQIPtrAssign2
, which requires that our underlying type T
derive from IUnknown
.
Okay, so here’s the scorecard for CComPtr
.
CComPtr scorecard |
|
---|---|
Default construction | Pass |
Construct from raw pointer | Pass |
Copy construction | Pass |
Destruction | Pass |
Attach and detach | Pass |
Assign to same-type raw pointer | Pass |
Assign to same-type smart pointer | Pass |
Fetch the wrapped pointer | p , or implicit conversion |
Access the wrapped object | -> (suboptimal) |
Receive pointer via & |
must be empty |
Release and receive pointer | & |
Preserve and receive pointer | &p |
Return to empty state | Pass |
Comparison | Pass |
Accidental bypass | Pass |
Construct from other-type raw pointer | Pass |
Construct from other-type smart pointer | Pass |
Assign from other-type raw pointer | Pass |
Assign from other-type smart pointer | Pass |
Notes:T may not be final .T must have a method of the form ULONG Release() .The T::Release method must return nonzero if the object is still alive. |
Netx time, we’ll look at WRL’s ComPtr
.
¹ Though you can hack around this by forcing a call to the base class version, or just using the raw wrapped pointer.
// sneaky! ducking under the ribbon! ptr->Test::Release(); ptr.p->Release();
The purpose of the “coloring” is not be be bulletproof. It’s just to prevent you from calling AddRef
and Release
by mistake.
The only similar accidental bypass prevention I know of originally used non-virtual methods but then switched to using `using` to make `AddRef` and `Release` private. (But it got removed because it was causing more problems that it was preventing.)