Aaargh! Part Three
I'm still at VSLive. Both my talks are done, so its just booth duty from here on in. The talks went... OK. Running VSTO on top of Virtual PC on a laptop was too slow; we'll have to devirtualize that for the next time.
Unfortunately they put me in the keynote room, which seats over a thousand. Now, when 200 people show up for a talk, that's great -- but in such an enormous room, it's really, really hard to get the audience enthusiam up when they're spread out over what seemed like a couple of acres. But I got a lot of good questions afterwards, so hopefully a few people were awake and learned something.
No time to write, so another canned gripe about C++ idioms that are drivin' me nuts today -- but first, an excerpt from Pirate Riddles For Sophisticates.
Q: Whom did the pirate vote for in the first Haitian election?
A: ARRRistide.
Q: Wait. Why did they let a pirate vote in the Haitian election?
A: Remember, the nation was taking its first halting steps toward democracy, and balloting procedures were rather chaotic. The pirate just slipped in somehow. Arrr.
Gripe #5: Exception Handling and operator new
In C++ you can easily change the new operator to throw an exception when it runs out of memory instead of returning NULL. Cool idea, but it can be very dangerous because this causes a global change in the behaviour of a commonly used operator. What happens when you do that in a program that contains libraries that do not have that assumption?
try
{
m_pbar = m_pfoo->bar();
} catch // ...
where you grabbed the source code for that class and just kind of compiled it in:
Bar * CFoo::bar(void)
{
HANDLE h = NULL;
h = GoGetMeAFileHandle();
m_pBlah = new CBlah;
if (NULL == m_pBlah){
FreeFileHandle(h);
return NULL;
} // ...
You didn't write CFoo::bar, but you've just broken its error handling if you redefined new to throw an error instead of returning NULL. Now if the allocation of CBlah fails, you've leaked a kernel object. This is another example where idioms like smart pointers need to be consistently applied across an application in order to reap their benefits.
I have debugged leaks in IIS where these kinds of things happen and they are absolutely no fun for our customers.
I once debugged some code where the new operator had been redefined to throw an exception, and the authors did something very clever. (Aside: clever is bad -- clever is hard to figure out! If the code is clever, rewrite it until it is brain-dead obvious.) In the debug build, they wrote some magic into the operator so that it detected if it was being called without an exception handler and raised an assertion. Sounds cool, right? Any possible negative consequences of that?
Well, how about the fact that static objects which have constructors that call new cannot be linked in to the application without the program asserting on startup? The standard map, vector, etc, objects are such objects. Their constructors call new, and therefore you can't have a static map. Statics have no context to catch the possible throw, and therefore will assert when the debug application starts up. (Guess how I found that bug?)
That said, I can see how it is useful to have a new operator that throws an exception. You know what I'd do if I wanted that? I'd define an operator overload that took an argument, create a dummy identifier called "throws", and then say pBar = new(throws) CBar -- now it is perfectly clear what the semantics of that thing are.
Redefining the global new operator works great if you have 100% control over all contexts where the operator is used. Unfortunately, that's frequently not a realistic assumption!
Comments
- Anonymous
March 25, 2004
What about the fact that the ISO C++ defines the default behavior of operator new to throw an exception? Presumably you're talking about developing software for non-compliant compilers? - Anonymous
March 25, 2004
I agree. Throwing is the default behavior and is what you should expect. If you don't want it to throw, you should call new (std::nothrow). I agree that redefining the global operator new to do something different is a bad idea - but the default behavior is to throw, so redefining it not to throw is what's a bad idea. - Anonymous
March 25, 2004
That's the nice thing about standards -- so many to choose from.
My (autographed!) copy of Stroustrup pretty clearly says that the default behaviour is to return NULL, and that it may be redefined to throw if that's what you want.
So what? That's ancient history. Well, lemme tell ya, we've got PLENTY of C++ code here that was written before any standard you care to name was a gleam in anyone's eye -- code that must continue to work as expected forever. I care a lot more about reality compliance than standards compliance!
And besides, you're missing my point. Whether the standard is "throw" or "return NULL", my point is that you can't have it both ways! Redefining basic operators to do stuff that is different than you expect is badness. The fact that some standards committee somewhere said that the redefined behaviour is the "right" thing isn't germane to my point. - Anonymous
March 25, 2004
I fully agree that when working on a codebase you have to be very careful to know what new does in that codebase. And, I think the standards committee standardizing on the behavior that wasn't the convention contributed to this problem. In fact, I just switched teams. For years I've been working with code where new throw, and now I'm going to code where new doesn't throw. It's very confusing.
My feeling is that anyone should upgrade their code so new throws (which is the default with most (all?) modern compilers) and convert their current code to use new (std::nothrow), unless it's prohibitively expensive. I'm not sure how you use STL or libraries like boost if you don't. However, there's plenty of code out there where this isn't worth the work involved. - Anonymous
April 22, 2004
C++ WAS A RESEARCH LANGUAGE
While introducing a great many innovations to the world at large, C++ was a new conglomeration of features whose collective effect could not have been known at the time of introduction.
The initial C++ team did not understand the full import of exceptions, of not having a built-in bool class, of the effect of overloadable shift operators on the paradigm for stream I/O, of the complexities of resolving overloaded functions (especially in namespaces), of not having a built-in string type, of template classes, and of the class version problem with regards to over-riding methods. And so on.
This is why there have been so many follow-efforts to produce a language like C++ but without all of its faults. C++ isn't a failure, it was research. On the other hand, C++ is not a success as a production language, as the current complaints about different kinds of new() operator attest. - Anonymous
June 05, 2004
[ Waste Book ] 04-06-05 - Anonymous
May 30, 2005
I don't get it, the standard makes it quite clear what happens here. Without the introduction of other operator new overloads that CFoo::bar sees, it calls the default new operator whose throw specification is std::bad_alloc. The standard says that only operator new with throw() as the throw-spec are the only ones allowed to return NULL on failure. The futher restrictions it places on operator new overloads and new_handler also enforce this. If you go break these rules, your code has undefined behavior, and anything can happen.
You cannot define a global operator new with a throw() throw-spec. The standard says there are implicit declarations of all the standard new operators in all translation units. If you define the default operator new but change the throw-spec to throw(), you are supposed to get a compiler error on conforming compilers due to the implicit declarations of them combined with the strict rules for duplicate function declarations. I think you should report this as a bug to the vendor of the compiler you use. - Anonymous
May 30, 2005
> I don't get it, the standard makes it quite clear what happens here
As I pointed out earlier, much of the legacy code we've got here was written before the Holy Standardizers decided to break backwards compatibility with the original language implementations.
That's the issue -- not what the standard says or doesn't say, but that it is very, very easy to get confused about what the actual behaviour is supposed to be when you're maintaining an unfamiliar bit of code.