16

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 and 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.

kunigami
  • 3,046
  • 4
  • 26
  • 42
  • Kind of duplicate, see http://stackoverflow.com/questions/1849490/ – Nikolai Fetissov Jul 22 '10 at 19:23
  • 2
    One problem with the Google style guide is that exceptions are getting more prevalent, not less, so that code that isn't exception-safe is brittle. – David Thornley Jul 22 '10 at 19:45
  • 2
    IMO, code that has its error-handling directly along side the normal code-flow isnt that readable either... Only exceptions enable you to separate normal code-flow and erro-handling. – smerlin Jul 22 '10 at 21:04
  • 7
    Google's problem is that they have an enormous codebase developed without exceptions (or exception-safety), so it would be an enormous effort to try to retrofit exception safety to it. You should not regard this as an argument against developing new code to use exceptions safely; just that, if you're already in a hole, it's easier to keep digging than to climb out. – Mike Seymour Jul 22 '10 at 23:36

5 Answers5

39

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.)

GManNickG
  • 494,350
  • 52
  • 494
  • 543
  • 26
    I hadn't heard the term "SBRM" before. It's much more descriptive than RAII. I hope it catches on. – Fred Larson Jul 22 '10 at 19:27
  • 2
    @Fred: Me too, that's why I'm using it everywhere. I'm going to make edits to wikipedia, and a CW Q&A on SO about it. I'm trying to [track down](http://stackoverflow.com/questions/3119197/whats-the-right-approach-for-error-handling-in-c/3119202#3119202) where the new term came from. – GManNickG Jul 22 '10 at 19:35
  • 2
    RAII is a lousy term, since what's really important is that Resource Relinquishment Is Destruction. SBRM is better, but I fear RAII is too entrenched. – David Thornley Jul 22 '10 at 19:47
  • 1
    I was going to -1 this answer because it really don't answer the question. For basic RAII issues, it is an ok answer but avoids some of the huge complexities of dealing with exceptions properly. – Torlack Jul 22 '10 at 19:49
  • 1
    @Torlack: Such as? The code guide, like @Martin states, is not something anyone should base new code off. It's a great guide to apply to pre-existing non-modern code, but that's it. (Also, do down-vote if you feel the need. If I got -2 my rep would be a multiple of 5, and for some reason it bugs me when it's not... :) ) – GManNickG Jul 22 '10 at 20:06
  • As far as the terms go: I've also always disliked "RAII" - I think both RRID and SBRM are much better, but they're still not great - all of them sound like "bleep-blorp-bloop" to me (not that I can think of anything better). Unfortunately, even if something really good comes around that didn't sound like "bleep-blorp-bloop", I think we're probably stuck with RAII... At least it's better than "SFINAE" - or is it? – Michael Burr Jul 22 '10 at 20:07
  • I was under the impression that RAII and RRID were slightly different. RRID essentially means the destructor cleans up resources held by the object. RAII does that as well, but it goes further in that resource acquisition happens when the object is constructed. – Adrian McCarthy Jul 22 '10 at 20:27
  • 1
    Nit: `std::auto_ptr` is deprecated. Perhaps the example should use `unique_ptr` or `shared_ptr`. – Adrian McCarthy Jul 22 '10 at 20:28
  • @Adrian: It's deprecated in C++0x, we aren't quite there yet. :) (Once we are I would have used `unique_ptr`, but since we aren't I'll stick to the standard one for now. I do recommend not actually using `auto_ptr` in real code.) – GManNickG Jul 22 '10 at 20:31
  • -1 for missing the boat. RAII saves you only the leaks associated with exceptions being thrown in the middle of code blocks. – Chris Becke Jul 22 '10 at 20:41
  • 1
    @Chris: As opposed to exceptions that are not thrown from the middle of code blocks? What exceptions would those be, and how do they concern us? If you use RAII for all resources, then those resources will be freed on exit from the scope block, whether by normal flow of execution, thrown exception, or structured or unstructured exit via return/break/goto. They won't be freed if you use `longjmp()`, which is an excellent reason not to use it. They may not be freed on `exit()`. I await clarification. – David Thornley Jul 22 '10 at 21:03
  • You only mentioned memory leaks, you totally ignored other issues with exception-safety.... RAII does NOT fix all your problems, you will need aditional helping idioms like e.g. copy-swap to make your code exception-safe – smerlin Jul 22 '10 at 21:07
  • I mean, RAII/SBRM cover only resource allocation. There are many other ways code can act incorrectly if an exception is thrown. See the Raymond Chen / Joel on Software articles. – Chris Becke Jul 22 '10 at 21:08
  • 3
    @Chris: If we're talking about the same Joel/Raymond articles, they didn't impress me. The Raymond Chen column Joel referenced showed no grasp of the use of RAII in exception safety, and understanding that role of RAII is sort of my minimum standard of who's worth reading on the subject. – David Thornley Jul 22 '10 at 21:16
  • 1
    @smerlin: Whether or not how exception safe something might be is an orthogonal issue; using SBRM you are guaranteed a basic exception guarantee. I'd say *most* strong exception guarantees come from proper, modern coding; simply using SBRM and maximum code re-use. (And so [copy-and-swap](http://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom) improves code re-use, which happens to give it a strong guarantee.) @Chris: I'm not familiar with those, I'll see if I can read them now and reply. – GManNickG Jul 22 '10 at 21:24
  • I don't understand the "and the code is cleaner". It isn't, it's more complicated and harder to read. It may be more correct but it is not cleaner. – Daniel Jul 22 '10 at 23:50
  • @Daniel: You no longer have to worry about cleaning anything up. There's nothing complicated about using a class. – GManNickG Jul 22 '10 at 23:56
  • I don't really follow the arguments against RAII in favor of SBRM or RRID. The only difference, in so far as I can see, is when the resource in question is initialized. You could very easily define an auto_ptr initialized to null, and initialize it later as result of another function call. My point: the 3 paradigms are arguing semantics and are otherwise indistinguishable. I'd argue all 3 are the same, regardless of whether the initialization is deferred or immediate. The strong point to be made in all three cases is: regardless of exit, all guarantee release of the resource. – Nathan Ernst Jul 22 '10 at 23:56
  • @Nathan: They *are* all the same, I don't think anyone ever said otherwise. :) We're just trying to come up with the best name for the idea you summarized: "regardless of exit, all guarantee release of the resource." I personally feel SBRM is best, is it emphasizes resource management is tied to scope. – GManNickG Jul 23 '10 at 00:05
  • 1
    @Gman, phew! Thought I was missing something there and was going to blame it on the excess of scotch I'm using to drain away all of today's stupid. – Nathan Ernst Jul 23 '10 at 00:19
  • 1
    @Daniel: This is probably a question of habit, you seem to perceive the more explicit delete statement as cleaner (why?), while in I perceive it as unnecessary clutter. – Fabio Fracassi Jul 23 '10 at 10:26
  • @DavidThornley "If you use RAII for all resources, then those resources will be freed on exit from the scope block, whether by...thrown exception..." Not quite. An unhandled exception calls std::terminate(), which does not necessarily call your destructors (see: https://akrzemi1.wordpress.com/2011/09/28/who-calls-stdterminate/). You are only guaranteed that your destructors run if you put something like this in `main`: `try { your_real_code(); } catch(...) { throw; }` – David Stone May 04 '12 at 20:53
  • @DavidStone: Which I would expect everyone does, sure. – GManNickG May 05 '12 at 06:38
9

"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.

James Curran
  • 101,701
  • 37
  • 181
  • 258
  • It sounds more like by "persistent state", they mean variables on the heap (by putting all of that together, they can account for exceptions better). – Brendan Long Jul 22 '10 at 19:33
  • 3
    Actually, by "persistent state", they almost certainly mean state observable by outsiders. It's more general than variables on the heap. See the work on software transactional memory, for instance. – Eric Brown Jul 22 '10 at 19:52
  • I agree with Eric. Some of the early problems with STL containers were how to deal with partial updates when a constructor throws an exception. That has nothing to do with local variables but with the bigger picture of the entity being updated. Now expand that to such things as files and databases. – Torlack Jul 22 '10 at 19:57
9

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.

bshields
  • 3,563
  • 16
  • 16
  • That doesn't give any reason to do them all at once though. For that situation you want small try/catch blocks. The only thing the exception is doing is forcing you to fix the problem, without them you'd still have the problem, you just wouldn't know about it. – Brendan Long Jul 22 '10 at 19:46
  • @Brendan - Exceptions really don't help much with the problem you are referring too. It just gives you a different method (and more clean IMHO) of dealing with the problem. Lazy people will still be lazy. – Torlack Jul 22 '10 at 19:53
  • 2
    @Brendan Long It's clearly possible to do correct error handling without exceptions. I prefer them, however, and as I said I don't agree with their reasoning here. But I think this definitely illustrates the argument that they're making, which was the point of the question. – bshields Jul 22 '10 at 19:54
  • does `baz->doSomething();` really throw a C++ exception? i thought it would throw a platform specific exception of some kind, that you cant catch in C++ catch handlers, but with a debugger only... – smerlin Jul 22 '10 at 21:02
  • @GMan, you're right. My memory of the standard was that it allowed such exceptions to be caught via `catch(...)` (without necessarily requiring it.) But AFAICT (via google), it's silent on the issue. I'm going to delete my original comment. – Dan Breslau Jul 22 '10 at 22:06
3

There are some articles that are worth reading, to understand why some people who are far smarter than I are wary of exceptions:

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++;
}
Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Chris Becke
  • 34,244
  • 12
  • 79
  • 148
  • 3
    The first Joel on Software article is from 2003, not long after people figured out what exception-safe actually was. The second one has little to do with exceptions, except pointing to the Chen article. The Raymond Chen article clearly demonstrates that Chen had no clue what he was talking about at the time (one hopes and expects he knows more of exception safety now). And, yes, that code is very likely wrong, but without knowing what it should do, and what the class invariants are, it's impossible to say. It is obvious that m_i might be incremented while m_j isn't. – David Thornley Jul 22 '10 at 21:20
  • 5
    I'll carry on our discussion from my answer here. To both points 1 and 2 in the first article I say: "So what?". I don't *need* to know if an exception might be thrown, because I understand if it does somethings gone bad. If I'm in a position to handle it, I will, otherwise who cares? Let someone higher in the chain solve the problem. I have no loss here. The second article is pure bogus crap: "I have a dosomething() and cleanup(), if dosomething throws I won't clean up!" Well freaking duh, that's why you put things in containers that clean up *for you*. Hell now your code is even cleaner... – GManNickG Jul 22 '10 at 21:32
  • 2
    ...because you don't even have to be explicit about cleaning up. I don't see the point to the third article. As far as I can tell it's just failed to recognize the purpose of exceptions. (Handle it if you can, and if not it'll automatically go to someone who can.) Much better than error codes (Better not forgot to handle it, or *nobody* will ever handle it.) As for your code snippet in your answer, I'm afraid I don't see the point. What exactly is wrong? If `SomeOtherMethod` throws an exception, *why would I want to continue?* – GManNickG Jul 22 '10 at 21:34
  • @GMan: SomeMethod could make cleanup on its own, or let the client of SomeMethod do it. The both solutions are right, as long as the "contract" between SomeMethod and the client is not breached. – Alsk Jul 23 '10 at 10:33
  • @Alsk: What your comment tells me is that Chris' code sample is insufficient to have a decent argument about. All I know about it is that it has one side effect before and one after a function call that can throw (assume *any* function call can throw unless there's a very good reason why not), that if there's a class invariant about keeping m_i and m_j in sync it's going to be violated, and that there's more coupling and less cohesion in the sample than I like. – David Thornley Jul 23 '10 at 14:35
  • @alsk: I'm with @David on this one, the sample doesn't make any point. And the "right" way is that, whatever that resource is that needs to be cleaned up shouldn't be cleaned up by either `SomeMethod` or the client of `SomeMethod`. Rather, it should be inside some container that will free it, and that container can be passed around. Now neither has to/cannot forget to clean up, it's guaranteed. – GManNickG Jul 23 '10 at 17:43
  • @GMan: Consider a case when SomeOtherMethod() throws exception, and all that the caller of SomeMethod will do about it is just terminate the program. Then all your hard work of wrapping every resource into a container responsible for cleanup would be just wasted time. Every written line of code must be exception-safe: false. Every written line of code must meet real user needs: true. Don't be religious ) – Alsk Jul 26 '10 at 09:04
  • 1
    @Alsk: I don't follow the terminate bit. I'm not being religious, I'm writing safe code. You can write unsafe, sloppy code if you want, I'm saying don't. – GManNickG Jul 26 '10 at 09:12
  • @GMan: I guess we would be talking about all these issues by means of postal mail, if everyone in the world had such strict beliefs. Sometimes [worse is better](http://www.jwz.org/doc/worse-is-better.html) – Alsk Jul 26 '10 at 10:44
  • @Alsk: That article is about Lisp, isn't it? I don't see how it applies to C++ at all, and I still don't get your terminate example. – GManNickG Jul 26 '10 at 19:26
  • @GMan: It's not about Lisp, but about two different philosophies in programming. The first states that "It is more important for the interface to be simple than the implementation.". The second - "It is more important for the implementation to be simple than the interface". If your code provides exception-safety then the interface to your code becomes simpler (the client doesn't have to worry about cleanup), but implementation of your code becomes more complicated (yes, I know that RAII simplifies it much, but in general it is true). So you are an apologist of the first philosophy. – Alsk Jul 27 '10 at 09:43
  • Excellent example and probably what Google refers too. Strong exception safety becomes a challenge in non trivial functions where any function invocation can possibly throw. The RAII idiom helps but is not sufficient alone for commit / rollback. – gast128 Mar 06 '23 at 13:19
3

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.

Community
  • 1
  • 1
Kevin Vermeer
  • 2,736
  • 2
  • 27
  • 38
  • 2
    You don't handle the same "errors" in the non-exceptional case, so it's apples and oranges. – ergosys Jul 22 '10 at 23:27
  • 2
    This code is a comparison of error handling versus doing nothing, not exceptions versus...well whatever you were trying to compare it to. – GManNickG Jul 22 '10 at 23:57
  • Point taken, so I added an `if (value != 1) { return -1 }` block. Nothing more is needed here This added less than 10 lines to the assembly. The point is that the exception system must be designed to handle all kinds of use cases, like thread safety, automatic destruction of objects, destruction of partially constructed objects (`throw` in a constructor), the list goes on - read the Technical Report. The program which doesn't need all this gets 118 lines of overhead. – Kevin Vermeer Jul 23 '10 at 00:35
  • You are basically completely wrong about what that quote means. It is refering to extra libraries neccessay to support writting exception safe code, like smart pointer classes. It explicitrly says "to make writting exception safe code easy" which has nothing at all to do with hardware. – Dennis Zickefoose Jul 23 '10 at 01:57