Recently while making a bug fix to our selection tracking code I discovered an unexpected behavior with CComPtr
As part of the fix I changed the type of a field (m_spTracking) from CComPtr
Or so I thought
I made my fix, ran our core check-in suites without error, checked in and moved onto the next bug. A couple hours later one of our other devs emailed me and informed me my check-in was breaking our larger, slower, suite bed run because m_spTracking was NULL. After some quick debugging I found myself looking at the following chunk of code which was apparently NULL’ing out m_spTracking in the suite.
CComPtr<IOldTracking> spOldTracking;
if ( SUCCEEDED(CreateOldSelectionTracking(&spOldTracking)) ) {
m_spTracking = spOldTracking;
}
Me and the other dev were quite shocked that this compiled at all. How is it possible to assign between CComPtr
template <typename Q>
T* operator=(_In_ const CComPtr<Q>& lp) throw()
{
if( !IsEqualObject(lp) )
{
return static_cast<T*>(AtlComQIPtrAssign((IUnknown**)&p, lp, __uuidof(T)));
}
return *this;
}
This templated operator allows for assignments between CComPtr instance no matter what the type is for the left and right side. The effect is that instead of doing compile type C++ type conversion rules, it will instead rely on runtime COM polymorphic assignment rules via IUnknown::QueryInterface. This moves assignment errors from compile time to runtime for unrelated interfaces.
This is further complicated because it only applies to assignment between CComPtr’s (and derived instances). If the right hand side of the assignment is a non-smart pointer, compile time C++ conversions will apply. To demonstrate ‘
CComPtr<ISelectionTracking> spTracking;
CComPtr<IOldTracking> spOld;
...
spTracking = spOld; // Fails at runtime
spTracking = (IOldTracking*)spOld; // Compilation Error
What surprised me though was talking to other developers about this issue. Most agreed with me that this is a bug in CComPtr
The fix in this case was pretty straight forward (use the new API) but I was still worried about how to prevent this type of problem in the future. In particular how to get the failure back to a compile time error. In the end we settled on using a stripped down version of CComPtr we already had in our code base going forward called CComPtrEx. I’ve previously blogged about about the need for this type here. It’s different from CComPtr in the following ways
- Does not have the templated version of operator= and instead relies on compile time C++ conversions checks for assignment
- Allows for interfaces which have multiple paths to IUnknown (can cause a compile time error in CComPtr).
Also for purposes of rigor, we temporarily commented out the CComPtr