March 26th, 2007

Passing by address versus passing by reference, a puzzle

Commenter Mike Petry asked via the Suggestion Box:

Why can you dereference a COM interface pointer and pass it to a function with a Com interface reference.

The call.

OutputDebugString(_T("IntfByRef::Execute - Begin\n"));
BadBoy badone;
CComPtr<IDoer> Doer;
Doer.CoCreateInstance(CLSID_Doer, NULL, CLSCTX_INPROC_SERVER);
// created a raw pointer - maybe the
// smart pointer was effecting it some how.
IDoer* Doer2;
Doer.CopyTo(&Doer2);
badone.stupid_method(*Doer2);
Doer2->Release();
// no still works.

The function called.

void stupid_method(IDoer& IDoerRef)
{
 IDoerRef.Do();
 CComQIPtr<IDispatch> WatchIt(&IDoerRef);
 if( WatchIt )
  OutputDebugString(_T("QI the address of the ")
                    _T("ref works - this is weird\n"));
 else
  OutputDebugString(_T("At least trying to QI the ")
                    _T("address of the ref fails\n"));
}

I found some code written like this during a code review. It is wrong but it seems to work.

You already know the answer to this question. You merely got distracted by the use of a COM interface. Let me rephrase the question, using an abstract C++ class instead of a COM interface. (The virtualness isn’t important to the discussion.) Given this code:

class Doer {
 public: virtual void Do() = 0;
};
void caller(Doer *p)
{
 stupid_method(*p);
}
void stupid_method(Doer& ref)
{
 ref.Do();
}

How is this different from the pointer version?

void caller2(Doer *p)
{
 stupid_method2(p);
}
void stupid_method2(Doer *p)
{
 p->Do();
}

The answer: From the compiler’s point of view, it’s the same. I could prove this by going into what references mean, but you’d just find that boring, but instead I’ll show you the generated code. First, the version that passes by reference:

; void caller(Doer *p) { stupid_method(*p); }
  00000 55               push    ebp
  00001 8b ec            mov     ebp, esp
  00003 ff 75 08         push    DWORD PTR _p$[ebp]
  00006 e8 00 00 00 00   call    stupid_method
  0000b 5d               pop     ebp
  0000c c2 04 00         ret     4
; void stupid_method(Doer& ref) { ref.Do(); }
  00000 55               push    ebp
  00001 8b ec            mov     ebp, esp
  00003 8b 4d 08         mov     ecx, DWORD PTR _ref$[ebp]
  00006 8b 01            mov     eax, DWORD PTR [ecx]
  00008 ff 10            call    DWORD PTR [eax]
  0000a 5d               pop     ebp
  0000b c2 04 00         ret     4

Now the version that passes by address:

; void caller2(Doer *p) { stupid_method2(p); }
  00000 55               push    ebp
  00001 8b ec            mov     ebp, esp
  00003 ff 75 08         push    DWORD PTR _p$[ebp]
  00006 e8 00 00 00 00   call    stupid_method2
  0000b 5d               pop     ebp
  0000c c2 04 00         ret     4
; void stupid_method2(Doer *p) { p->Do(); }
  00000 55               push    ebp
  00001 8b ec            mov     ebp, esp
  00003 8b 4d 08         mov     ecx, DWORD PTR _p$[ebp]
  00006 8b 01            mov     eax, DWORD PTR [ecx]
  00008 ff 10            call    DWORD PTR [eax]
  0000a 5d               pop     ebp
  0000b c2 04 00         ret     4

Notice that the code generation is identical.

If you’re still baffled, go ask your local C++ expert.

Mind you, dereferencing an abstract object is highly unusual and will probably cause the people who read your code to scratch their heads, but it is nevertheless technically legal, in the same way it is technically legal to give a function that deletes an item the name add_item.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

0 comments

Discussion are closed.