Temporaries

Bug++ of the Month

By Ron Burk, December 01, 2000

Bug++ of the Month | Dr Dobb’s (drdobbs.com)


Every so often, a reader just insists on doing all my work for me. Such is the case this month with a VC++ bug handed to me along with a description of the real-life code that led to it. It’s such a fundamental bug that I couldn’t believe we hadn’t covered it before, but after some searching, I can’t find any evidence it’s ever appeared in WDJ, so I’ll just turn this over to this month’s bug submitter.

John Burger Writes…

I’m not sure if this is a known bug, but was I surprised when I caused it! I’m using Microsoft Visual C++ v6.0 SP3. (It’s not a problem with Borland C++ v5.5.) The Microsoft Knowledgebase (Q151168) has this bug listed against VC++ v4.0 and reports it fixed under version 4.1, but the report is dated March 2000, so I’m confused! I’ll give some background first.

I’m the designer of most of the foundation classes we use at work, for other programmers to use in applications. So I’m careful to use C++ features to ensure that my “customers” (the other programmers) don’t misuse my classes. The most important of these is the use of the pure specifier (=0) on virtual functions that I really want people that are deriving from my classes to think about — even if it means simply calling the base class’ pure function. (Some programmers don’t realize that pure virtual functions can still have a body defined, although they can only be called via fully-specified names.)

As an aside, I find the syntax notation for pure functions difficult to isolate if I ever ask myself the question, “Which functions are pure?” To that end, our style guide has the following definition:

#define pure = 0

which allows a pure function to be defined as follows:

virtual void Fn() pure;

making pure nearly a keyword. Unfortunately, PC-Lint (a very useful tool!) complains about the #define — it warns that the equal sign is not required, which is a useful tip for newbies, but an irritation in this case!

Anonymous Variables. Another design style we use is the defining of classes that are complete within themselves. Merely constructing an instance of such class does all of the work that is needed. (The destructor, of course, cleans up as well.) A perfect example of this would be a message box: the constructor creates and displays it, waits for the user to click OK, and then returns. The destructor hides it and then destroys it. So, the line:

MessageBox message("The operation is complete");

does all of the work I need. Some compilers, however, complain about this with “Variable ’message’ is defined but never used”. To placate them, we make the variable anonymous:

MessageBox("The operation is complete");

which looks a lot like a function call, but is actually the creation and immediate destruction of an anonymous variable (more than a temporary variable, but not a full-blown named one).

Why don’t we use a function then? Using a class gives us the ability to use inheritance, it means that the different aspects of the “function” can be kept in different methods, and the state of the “function” can be kept in member variables, as close as you can get with C++ to Pascal’s nested functions.

That practice led me to the compiler bug, which can be boiled down to this example:

class Pure {
public:
    inline Pure(bool) {
    } // Pure(bool)
    virtual int Fn() = 0;
}; // Pure

int main() {
    Pure(false);
//  Pure test(true);
    return Pure(true).Fn();
} // main()

Note that Pure has a pure virtual function, which means it’s abstract. The first line of main() tries to instantiate an anonymous Pure, so it should fail. VC++ doesn’t complain! The last line of main() goes one step further — it tries to instantiate an anonymous Pure and then call Fn() on the anonymous variable. Again, VC++ doesn’t complain! If I un-comment the middle line, VC++ correctly complains about test being abstract. If I derive a class from Pure, but don’t override Fn(), then that class will also be abstract, and all of the above tests on the new class still behave the same.

OK, so it compiles and links (although it shouldn’t). How does it run? Not surprisingly, not very well. Using the debugger to follow the program flow, the first line jumps into the constructor and leaves again. The last line jumps into the constructor, leaves again, and then enters a runtime library function called void _purecall(void), a function that reports to the user that “a pure virtual function was called” and then terminates the program.

This is not a show-stopper, but the whole point of making a virtual function pure is so that the compiler will complain if a derived class doesn’t override it and prevent any instances of the abstract class. In this case, it doesn’t complain.

Microsoft’s Response

Sometimes I have to pore over language standards until my eyes hurt to research a bug, but this one is pretty easy. Not instantiating abstract classes is a very basic C++ language promise. I went to msdn.microsoft.com and searched for the keywords “bug C++ abstract”, but did not spot this bug in the 69 hits returned, so off the bug went to Microsoft for comment. Microsoft’s Kerry Loynd offered this response:

This is a regression of the VC 4.0 bug described in the article support.microsoft.com/support/kb/articles/Q151/1/68.asp. Note that it is only temporary objects that fail to generate the C2259 diagnostic. This bug will be fixed in the next major release of Visual C++.

I had discovered the bug report (Q151168) that both Kerry and John refer to, but did not consider it because when I compiled that bug report’s example on my machine, Visual C++ 6.0 quite correctly flagged the attempts to instantiate an abstract object. Unlike John’s bug, the bug in Q151168 looks like this:

/* Compile options needed: /c /GX
*/ 

class Exception    //abstract class
{
    int m_nCause;
    char * m_pszMsg;
public:
    Exception();
    Exception( int, const char * = 0);
    Exception( const Exception &);
    ~Exception();
    // NOTE: two pure virtual functions
    inline virtual int Cause() = 0;
    inline virtual
       const char * Msg() = 0;
};

int Exception::Cause()
    {
    return m_nCause;
    }

const char * Exception::Msg()
    {
    return m_pszMsg;
    }

void main()
    {
    try
    {
    int r = Exception(55,"no error")
          .Cause(); // ILLEGAL
    throw Exception(-1,
       "error"); // ILLEGAL
    }
    catch (Exception&e)
    {
    }
}

So, I would have to say that the bug in Q151168 really was fixed, while John’s bug is not. However, that raises an interesting question: how exactly was Microsoft able to fix the bug in Q151168 and still leave the bug that John found? Precisely, how are the two different? I spent one evening tweaking the two sources to try to figure out exactly what the conditions are under which the compiler suddenly starts realizing you’re trying to instantiate an abstract class, but couldn’t isolate the conditions. Maybe some reader with more ambition (and coffee) will solve the riddle. In any case, no matter how you cut it, Q151168 does not do a creditable job of telling Microsoft customers that John’s bug exists.