Last month’s bug was something of an anomaly for this column. Instead of exploring a problem unique to Microsoft or Borland, I took a look at a C++ language feature that gave both vendors trouble. The placement syntax used with operator new had one set of implementation problems with Microsoft, and a slightly different set with Borland.
This month, I take a look at another problem that seems to have slipped past the test suites of both companies. Oddly enough, two different compilers generate equivalent buggy code!
Member functions of C++ classes look and act much like the normal C functions I cut my teeth on long ago. In fact, C programmers can feel pretty comfortable with member functions if they view them as old-fashioned functions with a pair of superpowers:
This mental metaphor works pretty well for most member functions, but as usual, C++ has one more trick up its sleeve. In this case, the surprise is the static member function.
Static member functions give up one of the special attributes of member functions: they don’t have the implicit this parameter. This means that you can call a static member function either with or without an object of the appropriate class. For example, if bar is a static member function of class foo, you can call it several different ways:
void test( foo *foo1 ) { foo foo2; foo1->bar(); //Called using an object pointer foo2.bar(); //Called using an object reference foo::bar(); //Called without an object
Static member functions are generally used to manage the environment of an entire class of objects. For example, in Hewlett Packard’s release of the Standard Template Library, static member functions are used to manage memory pools for several container classes. Static member functions allocate big blocks of memory, then parcel out smaller pieces to individual members as necessary.
So what good is this capability? Static member functions have to give up the implicit use of an object. It would seem that static member functions could easily be replaced by global functions classified as friends of the given class.
While this is true, there are good reasons for using static member functions instead of global friend functions. Most important, static member functions help programmers adhere to the concept of encapsulation. If all the functions that modify objects of a given class are confined to the class definition, it becomes much easier to keep track of who does what. Encapsulation is one of the pillars that support object-oriented programming, and OOP, we are often told, is a good thing.
bug1195.cpp (Listing 1) shows a relatively short program that tests the ability of a compiler to properly use static member functions. The three subroutines, Test_1(), Test_2(), and Test_3() use three different techniques to call static member functions. Test_1() uses the most conventional technique, calling the function explicitly.
Test_2() and Test_3() call the static member function bar() by way of an object of class foo. The object in Test_2() is a local automatic variable; that in Test_3() is a temporary variable.
Class foo in bug1195.cpp contains some debug code that should be familiar to regular readers of this column. By printing out short messages when the object is created and destroyed, you can ensure that the compiler has generated the proper calls to constructors and destructors.
Sharp-eyed reader Aaron Margosis noticed that the output from this test program had a problem:
Test 1: bar Test 2: ctor bar dtor Test 3: bar dtor
The output from Test_3() indicates that the temporary foo object was destroyed without ever being properly constructed! This is clearly a major problem. Any moderately complex class is bound to corrupt data structures left and right when destroyed in this fashion.
The really odd part about this bug is that it is common to Borland C++ 4.5 and Visual C++ 1.5. Seeing a bug such as this in a mature product is surprising enough, but I would never have expected to see it pop up in two completely different places at once.
Brian Myers from Borland acknowledged the bug and said it will be fixed in a future release. Presumably, this will be Borland C++ 5.0.
John Browne from Microsoft had this to say:
This is fixed in our current product, Visual C++ 2.2.
Those of you using the 16-bit version of Visual C++ will presumably wonder if you can expect a fix as well.
Reader Margosis will be wearing his new WDJ t-shirt soon after this column sees print. You can join Aaron and several international supermodels on Mr. Blackwell’s Best-Dressed list by sending us your favorite C++ bugs. Just write them up and post them to us at wdletter@rdpub.com. With new releases of Borland’s and Microsoft’s compilers just around the corner, we’re hoping for a full mailbox!
Mark Nelson is a programmer for Greenleaf Software in Dallas, Texas. Mark is the author of The C++ Programmer’s Guide to the Standard Template Library, from IDG Books, as well as The Data Compression Book, from M&T Books. You can reach Mark on CompuServe at 73650,312.
// // This short program demonstrates a problem // found when calling a static member function. // For some reason, both VC 1.x and Borland 4.5 // call the destructor for a temporary object that // was never constructed properly. This error only // shows up in Test_3(), where a temporary object // is used to call a static member function. // #include <iostream.h> class foo { public: foo() { cout << "ctor "; } foo( const &foo ) { cout << "copy "; } ~foo() { cout << "dtor "; } static void bar() { cout << "bar "; } }; // // Test 1 performs properly. It doesn't construct // or destroy any foo() objects. // void Test_1() { foo::bar(); } // // Test 2 works properly as well. It constructs a // single object, then destroys it when the function exits. // void Test_2() { foo f; f.bar(); } // // Test 3 has a problem. It doesn't create the temporary // object used here, but it destroys it! // void Test_3() { foo().bar(); } void main() { for ( int i = 1 ; i <= 3 ; i++ ) { cout << "Test " << i << ": "; switch ( i ) { case 1 : Test_1(); break; case 2 : Test_2(); break; case 3 : Test_3(); break; } cout << endl; } }