On a disadvantage of exceptions in C++
I was reading Google C++ Style Guide, and got confused in the Exceptions part. One of the cons of using it, according to the guide is:
Exception safety requires both RAII and different coding practices. Lots of supporting machinery is needed to make writing correct exception-safe code easy. Further, to avoid requiring readers to understand the entire call graph, exception-safe code must isolate logic that writes to persistent state into a "commit" phase. This will have both benefits 开发者_如何学Goand costs (perhaps where you're forced to obfuscate code to isolate the commit). Allowing exceptions would force us to always pay those costs even when they're not worth
Specifically, the statement that I didn't understand is this:
(...) exception-safe code must isolate logic that writes to persistent state into a "commit" phase.
and this:
(...) perhaps where you're forced to obfuscate code to isolate the commit (...).
I think I'm not used to the terms "persistent state", "commit phase", "obfuscate code to isolate the commit". It'd be nice some small explanations, examples or references about these terms and possibly why this is true.
Basically, modern C++ uses Scope-Bound Resource Management (SBRM, or RAII). That is, an object cleans up a resource in its destructor, which is guaranteed to be called.
This is all fine and dandy, unless your code isn't modern. For example:
int *i = new int();
do_something(i);
delete i;
If do_something
throws an exception, you've leaked. The correct solution is to not have the resource out in the wild like that, i.e.:
std::auto_ptr<int> i(new int());
do_something(i.get());
Now it can never leak (and the code is cleaner!).
I think what the guide is trying to say is that we already have all this old code, and using a modern style would take too much effort. So let's not use exceptions. (Rather than fix all the code...I dislike the Google Style Guide very much.)
"writes to persistent state" mean roughly "writes to a file" or "writes to a database".
"into a 'commit' phase." means roughly "Doing all the writing at once"
"perhaps where you're forced to obfuscate code to isolate the commit" means roughly "This may make the code hard to read" (Slight misuse of the word "obfuscate" which means to deliberately make something hard to read, while here they mean inadvertantly make it hard to read, but that misuse may have been intentional, for dramatic effect)
Elaborating more: "writes to persistent state" more closely means "Write out, to some permanent media, all the details about this object that would be needed to recreate it". If writing was interrupted by an exception, then those "written out details" (i.e. "persistent state") could contain half the new state and half the old state, leading to an invalid object when it was recreated. Hence writing the state must be done as one uninterruptable act.
What it's saying about persistent state is this: even if you're using RAII and your object gets destructed properly, allowing you to clean up, if the code in the try block modified the state of the system in some way, you most likely need to figure out how to roll back those changes because the operation didn't complete successfully. They use the term commit here as it relates to transactions, the notion that when you execute an operation, the state of the system should be as if it was completed 100% successfully or it didn't happen at all.
Here's how this can get messed up even with RAII:
struct MyClass
{
MyClass(Foo* foo)
{
m_bar = new Bar;
foo->changeSomeState();
}
~MyClass()
{
delete m_bar;
}
Bar* m_bar;
};
Now if you have this code:
try
{
MyClass myClass(foo);
Baz baz;
baz.doSomething(); // Throws an exception
}
catch(...)
{
// MyClass doesn't leak memory, but should it try to undo
// the change it made to foo?
}
So to handle this kind of case correctly, you have to add more code to treat this as a transaction and to roll back whatever changes to persistent state were made in the try block when an exception is thrown. They're just saying that forcing transaction semantics can clutter up (obfuscate) the code.
I don't agree with banning exceptions, btw, just trying to show the problem they're referring to.
Lots of supporting machinery is needed to make writing correct exception-safe code easy.
I'm surprised that more people didn't key into this line. This is the 'con' being discussed: Exception handling is expensive. The rest of the paragraph is just the details of why so much machinery is required.
This is a disadvantage of exceptions that is usually overlooked on dual-core 2GHz machines with 4GB of RAM, a 1TB hard drive, and gobs of virtual memory for every process. If the code is easier to understand, debug, and write, then buy/make faster hardware, and write bottlenecks without exceptions and in C.
However, on a system with tighter constraints, you can't ignore the overhead. Try this. Make a test.cpp file like this:
//#define USE_EXCEPTIONS
int main() {
int value;
#ifdef USE_EXCEPTIONS
try {
#endif
value++;
#ifdef USE_EXCEPTIONS
if (value != 1) {
throw -1;
}
}
catch (int i) {
return i;
}
#else
return -1;
}
#endif
return value;
}
As you can see, this code does next to nothing. It performs an increment on a static value.
Compile it anyways with g++ -S -nostdlib test.cpp
and look at the resulting test.s assembly file. Mine was 29 lines long without the if (value != 1) { return -1 }
block, or 37 lines with the example return test block. Much of that was labels for the linker.
After you're satisfied with this code, uncomment the #define USE_EXCEPTIONS
option at the top, and compile again. Wham! 155 lines of code to handle the exception. I'll grant you that we now have an extra return
statement and an if
construct, but these are only a couple lines each.
This is far from a complete exception handling benchmark. See the ISO/IEC TR18015 Technical Report on C++ Performance, section 5.4, for a more authoritative and thorough answer. Do note that they start with the almost-as-trival example:
double f1(int a) { return 1.0 / a; }
double f2(int a) { return 2.0 / a; }
double f3(int a) { return 3.0 / a; }
double g(int x, int y, int z) {
return f1(x) + f2(y) + f3(z);
}
so there is merit in using absurdly small test cases. There are also StackOverflow threads here and here (where I pulled the above link from, courtesy Xavier Nodet).
This is the supporting machinery that they were talking about, and it's why 8GB of RAM will soon be standard, why processors will have more cores and run faster, and why the machine you're on now will be unusable. When coding, you should be able to peel the abstraction away in your head and think of what the line of code really does. Things like exception handling, run time type identification, templates, and the monstrous STL are expensive in terms of memory and (to a lesser degree) runtime. If you've got lots of memory and a blazing CPU, then don't worry about it. If not, then be careful.
There are some articles that are worth reading, to understand why some people who are far smarter than I are wary of exceptions:
- Joel on Software - 13 - Exceptions
- Joel on Software - Making Wrong code look Wrong
- The Old New Thing - Cleaner, more elegant, and harder to recognize
My favorite example is: if you use exceptions, this code is very likely wrong - and its impossible to tell just by looking at it:
void SomeMethod(){
m_i++;
SomeOtherMethod();
m_j++;
}
精彩评论