views:

454

answers:

6

I'm in the process of porting a C++/WTL project from Visual Studio 2005 to VS 2008. One of the project configurations is a unit-testing build, which defines the preprocessor symbol UNIT_TEST.

In order to get my WTL classes into my test harness, I made a CFakeWindow class that stubs all the CWindow methods. Then in my stdafx.h file, I do this right below the import of atlwin.h (which defines the CWindow class):

#ifdef UNIT_TEST
#include "fakewindow.h"
#define CWindow CFakeWindow
#endif

My window classes then look like this:

class CAboutDialog : 
    public CDialogImpl< CAboutDialog, CWindow > 
    , public CDialogResize< CAboutDialog >
{
// class definition omitted...
};

This works great in VS 2005. The problem is that in VS 2008, the methods from the original CWindow class are getting called, instead of the CFakeWindow class. This of course causes the tests to fail, because CWindow is sprinkled with ATLASSERT(::IsWindow(m_hWnd)).

When I step through the code in the debugger, I see that the CAboutDialog class is inheriting from CDialogImpl<CAboutDialog, CFakeWindow>. Yet when I call a method on CAboutDialog (e.g. EndDialog(code)), the CWindow method is getting called.

Is this a bug in VS 2008, or was my conditional template inheritance technique an abomination that VS 2005 allowed but VS 2008 "fixed"? Is there a work-around, or do I need to consider a different technique to unit-test WTL classes? I really like this technique, because it lets me get WTL classes into a test harness without mucking about with the WTL library.

Edit: As noted in the response to Conal, below, the preprocessor output shows that my class is inheriting from CFakeWindow:

class CAboutDialog :
    public CDialogImpl<CAboutDialog, CFakeWindow >
    , public CDialogResize< CAboutDialog >
    ...

And as stated above, when I step through the code in the debugger, CAboutDialog is shown in the locals window as inheriting from CFakeWindow.

Edit 2: As per Conal's advice, I stepped through the disassembly, and the code is supposedly calling the CFakeWindow method, but the CWindow method is what is actually called.

     if ( wID == IDCANCEL ) 
00434898  movzx       edx,word ptr [ebp+8] 
0043489C  cmp         edx,2 
0043489F  jne         CAboutDialog::OnCloseCmd+90h (4348B0h) 
     {
      EndDialog( wID ) ;
004348A1  movzx       eax,word ptr [ebp+8] 
004348A5  push        eax  
004348A6  mov         ecx,dword ptr [ebp-10h] 
004348A9  call        ATL::CDialogImpl<CAboutDialog,ATL::CFakeWindow>::EndDialog (40D102h) 
     }
     else
004348AE  jmp         CAboutDialog::OnCloseCmd+9Ah (4348BAh) 
     {
      EndDialog(IDOK);
004348B0  push        1    
004348B2  mov         ecx,dword ptr [ebp-10h] 
004348B5  call        ATL::CDialogImpl<CAboutDialog,ATL::CFakeWindow>::EndDialog (40D102h)

I'm starting to lean more toward a bug in the VC++ 2008 debugger.

+1  A: 

Have you looked at the preprocessor output to confirm that your hack is in fact doing what you think? Using the preprocessor to rename classes that are part of a template library is fraught with danger. Also check that the signature of each and every method in your CFakeWindow exactly matches CWindow.

VS2008 has been around for quite a long time now, so I would not be considering a compiler bug until you have first eliminated all other possibilities.

Conal
Yes, the preprocessor output gives "public CDialogImpl<CAboutDialog, CFakeWindow >"
Ryan Ginstrom
Sorry, to answer your second question: the signatures match exactly, because I made the CFakeWindow class by taking the CWindow code and basically scooping out the method content.
Ryan Ginstrom
Your approach ought to work in principle, but I would still want to examine the generated code very closely to follow what is happening when a class method is invoked. One risk is that there is some non-template base class code somewhere that is outside the scope of your #define and so references CWindow, not CFakeWindow. Have you single-stepped (at the machine instruction level) to see what happens when EndDialog() is invoked?
Conal
I haven't stepped through the disassembly, but I'll note that this worked fine in VS 2005, and I made no changes to the code compiling with VS 2008. I'll try this, though.
Ryan Ginstrom
+2  A: 

I think this is a long shot, but is it possible that precompiled headers are playing havoc with your build? Try turning them off in your project (if they're on) and do a clean build to see if you get any better behavior.

Michael Burr
I haven't turned off precompiled headers, but I deleted all binary files and rebuilt when I moved to VS 2008. If just using precombiled headers causes the error, then I'd still say it's a bug in VS. I might try not using precompiled headers at all if no other solutions are found, but I'd frankly rather muck about in the WTL code than get rid of precompiled headers.
Ryan Ginstrom
Given what you've said it's almost certainly not the issue. Actually from the start I thought the chances of this being the problem were minimal (near zero), but I also figured it's an easy thing to check - just turn them off for one build and see if there's any change. If turning them off fixes the problem but you don't want to live without them, at least you've narrowed the scope of the problem area to look at.
Michael Burr
+1  A: 

Try something like:

class CAboutDialog :
    public CDialogImpl<CAboutDialog, FAKE_CWindow_MACRO >

That way you can stop trying to use the preprocessor to rewrite system header files, and use it for your own code.

Yes, I know you have to "refactor" your code, but it's a search-and-replace job.

Your current technique is something of a hack because it makes an unwarranted assumption: that no source file will #include any header from ATL that relies on a consistent definition of CWindow. This is not likely to be stable as code changes over time.

Marsh Ray
This kind of "refactoring" isn't a hassle -- hard-coding the location of the stdafx.h file in libraries spread in multiple directories and shared by other projects would be. But unfortunately, this method didn't work either (see TWindow discussion above).
Ryan Ginstrom
+1  A: 

Then in my stdafx.h file, I do this

AFAIK, stdafx.h is a precompiled header by default. So when you define UNIT_TEST macro, it doesn't affect precompiled file, which still has CWindow as template argument.

Pavel Shved
Good idea, but as I noted in my question, the preprocessor output has "CWindow" transformed to "CFakeWindow" in the template inheritance.
Ryan Ginstrom
Are you sure that preprocessor output corresponds with what precompiled header file contains?
Pavel Shved
Pretty sure. Please see the disassembler code that I added to my question.
Ryan Ginstrom
What would make me sure is if you have removed all built files from your directory (including precompiled header) and, on this clear space, built the project with `UNIT_TEST` at once (without doing any previous build).
Pavel Shved
Yes, I completely deleted the build directory several times.
Ryan Ginstrom
+4  A: 

This might be a clearer way of handling it:

#ifdef UNIT_TEST
#include "fakewindow.h"
#define TWindow CFakeWindow
#else
#define TWindow CWindow
#endif

Perhaps there's a case where the redefine is not getting through the precompiled headers. If so, this will catch any such problem.

Billy ONeal
This looked promising, but unfortunately I get the same problem. CAboutDialog is shown in the locals window as inheriting from CFakeWindow, but when I step into a TWindow method, it goes to CWindow.
Ryan Ginstrom
This did work! I'm an idiot -- I didn't step all the way into the IsWindow call of EndDialog, which is called on CFakeWindow now. Thank you!
Ryan Ginstrom
+1  A: 

I see the library symbols are in the namespace ATL. Is that perhaps throwing off your macro trickery?

In general, I would not count on any debugger to report template instantiations perfectly. Too many weird optimizations are required to happen even in Debug builds.

Marsh Ray
I also put the CFakeWindow class in the ATL namespace. I needed to do this, because the WTL controls tend to declare "ATL::CWindow" (probably legacy from the MFC CWindow class screwing up intellisense).
Ryan Ginstrom
Is CFakeWindow a template? If so, I would use http://www.boost.org/doc/libs/release/libs/static_assert/static_assert.htm BOOST_STATIC_ASSERT(0) inside the template member function you think should be called. This will tell you if the compiler is actually instantiating it.
Marsh Ray