The other day, I was testing COM clients which accessed a collection class
via a COM-style enumerator (
IEnumVARIANT
). And those clients crashed
as soon as they tried to do
anything with the enumerator. Of course, the
same code had worked just fine all the time before. What changed?
In COM, a collection interface often implements a function called
GetEnumerator()
which returns the actual enumerator interface (
IEnumVARIANT
), or rather,
a pointer to the interface. In my case, the signature of that function was:
HRESULT GetEnumerator(IUnknown **);
Didn't I say that
GetEnumerator
is supposed to return an
IEnumVARIANT
pointer? Yup, but for reasons which I may cover here in one
of my next bonus lives, that signature was changed from
IEnumVARIANT
to
IUnknown
.
This, however, is merely a syntactic change - the function actually still
returned
IEnumVARIANT
pointers, so this alone didn't explain the crashes.
Well, I had been
bitten before by smart pointers,
and it happened again this time! The COM client code declared a smart
pointer for the enumerator like this:
CComPtr<IEnumVARIANT> enumerator = array->GetEnumerator();
This is perfectly correct code as far as I can tell, but it causes a fatal
avalanche:
- The compiler notices that
GetEnumerator
returns an IUnknown
pointer.
This doesn't match the constructor of this particular smart pointer
which expects an argument of type IEnumVARIANT *
.
- So the compiler looks for other matching constructors.
- It doesn't find a matching constructor in
CComPtr
itself,
but CComPtr
is derived from CComPtrBase
which has
an undocumented constructor CComPtrBase(int)
.
- To match this constructor, the compiler converts the
return value of
GetEnumerator()
into a bool
value which
compresses the 32 or 64 bits of the pointer into a single bit!
(Ha! WinZip, can you beat that?)
- The boolean value is then passed to the
CComPtrBase(int)
constructor.
- To add insult to injury, this constructor doesn't even use its argument
and instead resets the internally held interface pointer to 0.
Any subsequent attempt to access the interface through the smart pointer now crashes
because the smart pointer tries to use its internal interface pointer - which is 0.
All this happens without a single compiler or runtime warning. Now, of course it
was our own silly fault - the
GetEnumerator
declaration was bogus.
But neither C++ nor ATL really helped to spot this issue.
On the contrary, the C++ type system (and its implicit
type conversions) and the design of the ATL smart pointer classes
collaborated to hide the issue away from me until it was too late.