Just when were fixin to swear at VC++ again, it turns around and does something right.
Copyright © 2001 Robert H. Schmidt
Writing for a print magazine can be quite humbling. Once I commit something to print, I lose my last chance for immediate retraction or clarification. Every word lives forever.
My only real opportunity for change errata in later columns is far from ideal. Errata logically are revision marks that physically live in a distinct piece; both the original and the errata are required to make the intended writing complete. Anyone reading just the original will get a partial picture. The full picture is distributed across space and time.
This is one large reason Im preferring more and more to write for online publications: I can make corrections directly into the one canonical master copy, a form of iterative improvement and debugging on live published material. The moving finger writes and, having writ, can move back.
Bug-- of the Month
Q
Mr. Schmidt:
The attached file causes VC++ v6 to complain and threaten not to release memory.
If the destructor declaration in class X is made non-virtual, or if it is just removed, the files compile fine without complaint. I have not had an opportunity to try compiling this with any other compiler. I am not sure whether I have tripped over some C++ obscurity or a compiler bug.
I would appreciate your comments.
Regards Bill Hay
A
Ron Burk recently started soliciting potential authors to write WDJs Bug++ of the Month column [1], which almost always picks apart some problem in Visual C++. Temporarily disregarding my day job, I spent about six seconds flirting with the notion of auditioning for that position. Sanity regained hold quickly, if only because I get an opportunity to write such a column here in CUJ.
Ive reduced your sample to
#include <stddef.h> class X { public: virtual ~X(); void *operator new(size_t); void operator delete (void *, size_t); }; X *x = new X; // warning hereWhen compiled, this code produces the warning
no matching operator delete found; memory will not be freed if initialization throws an exceptionYouve run into a pair of Visual C++ compiler bugs. Ill describe whats supposed to happen and then demonstrate how Visual C++ is actually behaving.
For the usual case of
x = new Xthe program calls the non-placement allocation function
operator new(size_t)to allocate space for *x. In the more general case of
x = new(a, b, c) Xthe program calls the placement allocation function
operator new(size_t, A, B, C)to allocate the space, where the arguments a, b, and c are passed in as parameters to operator new. In all cases, the corresponding expression
delete xcalls either
operator delete(void *)or
operator delete(void *, size_t)to free the allocated space, depending on which overload is available [2]. The void * parameter points to *xs space. In the second overload, the size_t parameter gives the size of that space. (This matches the size_t parameter in the operator new call.)
Now consider what happens during
x = new(a, b, c) Xif the X constructor throws. The throw occurs after the call to
operator new(size_t, A, B, C)has succeeded, meaning the space has already been allocated. Unless you wrap the new expression in a try block and manually free the memory in an exception handler, your program will suffer a memory leak.
To avoid this problem, you can arrange for the memory to be freed automatically. According to the language rules, the program will call the placement deallocation function
operator delete(void *, A, B, C)to free the space if the X constructor in
new(a, b, c) Xthrows [3]. In the usual case of
new Xthere are no placement arguments; instead, the program automatically calls a non-placement deallocation function to free the memory.
Thus, for the general case of
operator new(size_t, A, B, C)there are three potentially matching deallocation functions:
- operator delete(void *) called via delete x
- operator delete(void *, size_t) called via delete x
- operator delete(void *, A, B, C) called via an X constructor throw
Note that the second operator delete overload is a special case of the third, where A, B, C is replaced by size_t. This second overload is called automatically when the X constructor throws in
size_t n; x = new(n) X;In this instance, operator delete serves as a placement deallocation function matched to the placement allocation function
operator new(size_t, size_t) // ^ ^ argument n // ^ size of spaceThus, this same operator delete overload serves two distinct roles:
- A non-placement deallocation function matching every allocation function and called via delete x. The size_t parameter is the size of the allocated space.
- A placement deallocation function matching operator new(size_t, size_t) and called via X constructor throw. The size_t parameter is the original placement argument n to operator new.
(Interesting implication: operator delete(void *, size_t) does not inherently know how it was called, or how to interpret its extra parameter. This is the only deallocation function to suffer these twin ambiguities [4].)
With that background, its time to analyze Visual C++.
Build and run my test program (Listing 1) with Visual C++. Youll find that three of the four test cases run correctly. The only failure comes in test 3: the program is supposed to automatically call operator delete but doesnt. That failed test case represents compiler bug #1.
At compile time, youll also see two instances of the familiar compiler warning: one for test 1, and one for test 3. However, test 1 actually runs correctly. That first warning is a red herring and represents compiler bug #2.
(Ill note for the sake of completeness that my other two test systems Metrowerks CodeWarrior Pro v6 for Mac OS, and EDG v2.45 for MS-DOS get all test cases right without spurious warnings.)
Advantage Microsoft
Q
C# and Java: What is the difference? When will there be a lawsuit from Sun, and what happened to J++? Thanks Bill Eidson
A
Since Im watching the Australian Open as I type, Ill take my inspiration from John McEnroe: You cannot be serious!
You Can Run, but You Cant Hide
Q
I have tried compiling the attached code on five different platforms (three Unix vendor-supplied compilers, gcc, and MSVC 6.0), and only MSVC 6.0 successfully compiles it. The rest complain something to the effect of No matching function for call to Override::func(int,int), etc.
One even complains about func being redeclared after the Base::func; access declaration. Not all the mentioned compilers support the using Base::func; declaration, so I went with the least common denominator syntax.
So, given that I cant change the existing Base API, what is the correct format for this combination of overloading and overriding? Is there one? Michael McCarty
A
Well, this is a nice turn about: the Charlie Brown of compilers goes from being the goat in the first question to being the hero here. My stock options are pleased.
Your code appears as Listing 2, with one slight modification: Ive replaced <iostream.h> with <iostream>. That first header is not part of the Standard C++ library, even though many library vendors support it. I strongly recommend that you prefer <iostream> wherever you can.
Now to your real question.
Your code should build and run, giving the output
Override::func() Base::func(int) Base::func(int, int)The key is the access declaration
class Override : public Base { public: Base::func; // access declaration void func(); };which trumps the usual base-member hiding and overriding rules. From Subclause 11.3:
The access of a member of a base class can be changed by mentioning its qualified-id in the derived class declaration. Such mention is called an access declaration. The effect of an access declaration qualified-id; is defined to be equivalent to the declaration using qualified-id;.
The Standard defines an access declaration as equivalent to a using declaration, which (from 7.3.3):
...introduces a name into the declarative region in which the using-declaration appears. That name is a synonym for the name of some entity declared elsewhere.
The member name specified in a using-declaration is declared in the declarative region in which the using-declaration appears.
When a using-declaration brings names from a base class into a derived class scope, member functions in the derived class override and/or hide member functions with the same name and parameter types in a base class (rather than conflicting).
For the purposes of overload resolution, the functions which are introduced by a using-declaration into a derived class will be treated as though they were members of the derived class.
The name in an access declaration acts as if it were physically declared in the scope of the access declaration. In your particular example, the access declaration
Base::func;appears in the scope of Override and thus effectively declares all of the Base::func overloads within Override.
At first glance, this would seem to give Override four func overloads:
- func() declared directly within Override
- func() introduced from Base
- func(int) introduced from Base
- func(int, int) introduced from Base
But as the Standard provides, the two func() declarations dont conflict; instead, Override::func() overrides Base::func(). Class Override therefore effectively declares three func overloads: one directly, and two via the access declaration.
I suspect your errant compilers are either ignoring the access declaration or are misapplying the Standards rules regarding access declarations.
As the Standard indicates, and as you mention, you should be able to rewrite Override as
class Override : public Base { public: using Base::func; // using declaration void func(); };This rewrite employs using-declaration syntax instead of access-declaration syntax. As the Standard indicates in Footnote 100:
Access declarations are deprecated; member using-declarations (7.3.3) provide a better means of doing the same things. In earlier versions of the C++ language, access declarations were more limited; they were generalized and made equivalent to using-declarations in the interest of simplicity. Programmers are encouraged to use using-declarations, rather than the new capabilities of access declarations, in new code.
Had you declared Override with neither an access declaration nor a using declaration:
class Override : public Base { public: void func(); };the function Override::func would have hidden the Base functions func(int) and func(int, int), and the calls
o.func(1); o.func(1, 1);would not have compiled. Instead, you would have needed to qualify the func calls as
o.Base::func(1); o.Base::func(1, 1);Notes
[1] WDJ = Windows Developers Journal, a sister publication of CUJ. Strange but true: Ron Burk and our own Marc Briand were college roommates back in the Carter administration. [Kinda scary, isnt it? mb]
[2] If both are available, operator delete(void *) wins. See Subclause 3.7.3.2.
[3] Provided the appropriate operator delete overload exists and is unambiguously available. Otherwise, no automatic deallocation occurs, and the memory is leaked. Subclause 5.3.4 has all the details.
[4] Under normal operating conditions, that is. Nothing prevents you from manually calling operator delete as you would any other function and passing any arguments you want.
Although Bobby Schmidt makes most of his living as a writer and content strategist for the Microsoft Developer Network (MSDN), he runs only Apple Macintoshes at home. In previous career incarnations, Bobby has been a pool hall operator, radio DJ, private investigator, and astronomer. You may summon him on the Internet via BobbySchmidt@mac.com.