开发者

Exception Safety- When, How, Why?

开发者 https://www.devze.com 2023-01-31 02:57 出处:网络
I\'m just a fledgling programmer that at least tries to program more than the best-case scenario. I\'ve been reading Herb Sutter\'s \"Exceptional C++\" and went through the exception-safety chapters t

I'm just a fledgling programmer that at least tries to program more than the best-case scenario. I've been reading Herb Sutter's "Exceptional C++" and went through the exception-safety chapters thrice so far. However, barring the example he posed (a Stack), I'm not really sure when exactly I should strive for exception safety vs speed and when it's just plain silly to do so.

For example, my current homework project is a doubly-linked list. Since I've programmed a couple of these already, I wanted to take the time to get into some deeper concepts such as ES.

Here is my pop-front function:

void List::pop_front()
{
    if(!head_)
        throw std::length_error("Pop front:  List is empty.\n");
    else
    {
        ListElem *temp = head_;
        head_          = head_->next;
        head_->prev    = 0;
        delete temp;
        --size_;
    }
}

I had some dilemmas with this.开发者_Go百科

1) Should I really throw an error when a list fails? Shouldn't I rather simply do nothing and return instead of forcing the user of the list to perform try {] catch() {} statements (that are also slow).

2) There are multiple error classes (plus the ListException my teacher demands we implement in the class). Is a custom error class really necessary for such a thing, and is there a general guide on when to use a specific exception class? (For example, range, length and boundary all sound alike)

3) I know I shouldn't change the program state until all that code that has thrown an exception be done. This is why I'm decrementing size_ last. Is this really necessary in this simple example? I know delete can't throw. Is it possible for head_->prev to ever throw when assigning to 0? (head is the first Node)

My push_back function:

void List::push_back(const T& data)
{
    if(!tail_)
    {
        tail_ = new ListElem(data, 0, 0);
        head_ = tail_;
    }
    else
    {
    tail_->next = new ListElem(data, 0, tail_);
    tail_ = tail_->next;
    }
    ++size_;
}

1) I hear often that anything can fail in a C++ program. Is it realistic to test if the constructor for ListElem fails (or tail_ during newing)?

2) Would it ever be necessary to test the type of data (currently a simple typedef int T until I templatize everything) to make sure the type is viable for the structure?

I realize that these are overly simple examples, but I'm currently just confused as to when I should actually practice good ES and when it's not.


Should I really throw an error when a list fails? Shouldn't I rather simply do nothing and return instead of forcing the user of the list to perform try {] catch() {} statements (that are also slow).

Absolutely throw the exception.

The user must know what happened if the list was empty - otherwise it will be hell to debug. The user is not forced to use try/catch statements; if the exception is unexpected (i.e. can only occur due to programmer error), then there is no reason to try to catch it. When an exception goes uncaught, it falls through to std::terminate and this is very useful behaviour. The try/catch statements themselves aren't slow, either, anyway; what costs is the actual throwing of the exception and unwinding of the stack. It costs approximately nothing if the exception doesn't get thrown.

There are multiple error classes (plus the ListException my teacher demands we implement in the class). Is a custom error class really necessary for such a thing, and is there a general guide on when to use a specific exception class? (For example, range, length and boundary all sound alike)

Be as specific as you can. Using your own error classes is the best way to do this. Use inheritance to group related exceptions (so that callers can catch them more easily).

I know I shouldn't change the program state until all that code that has thrown an exception be done. This is why I'm decrementing size_ last. Is this really necessary in this simple example? I know delete can't throw. Is it possible for head_->prev to ever throw when assigning to 0? (head is the first Node)

If head_ is null, then dereferencing it (as part of the attempt to assign to head_->prev) is undefined behaviour. Throwing an exception is a possible consequence of undefined behaviour, but an unlikely one (it requires the compiler to be going out of its way to hold your hand, in a language where that sort of thing is considered absurd ;) ), and not one that we worry about, because undefined behaviour is undefined behaviour - it means your program is already wrong anyway, and there's no point in trying to make the way in which it's wrong be more right.

Plus, you're already explicitly checking that head_ isn't null anyway. So there's no problem, assuming you aren't doing anything with threading.

I hear often that anything can fail in a C++ program.

That's slightly paranoid. :)

Is it realistic to test if the constructor for ListElem fails (or tail_ during newing)?

If the new fails, then an instance of std::bad_alloc is thrown. Throwing an exception is exactly what you want to happen here, so you don't want or need to do anything - just let it propagate. Re-describing the error as some kind of list exception is not really adding useful information and may just obscure things further.

If the constructor ListElem fails, it should fail by throwing an exception, and it's about 999 to 1 that you should just let that one fall through, too.

The key here is that whenever an exception gets thrown here, there is no cleanup work to do, because you haven't modified the list yet, and the constructed/newed object Officially Never Existed(TM). Just make sure that its constructor is exception-safe, and you'll be fine. If the new call fails to allocate memory, the constructor doesn't even get called.

The time when you have to worry is when you are making more than one allocation in the same place. In this case, you have to make sure that if the second allocation fails, you catch the exception (no matter what it is), clean up the first allocation, and re-throw. Otherwise, you leak the first allocation.

Would it ever be necessary to test the type of data (currently a simple typedef int T until I templatize everything) to make sure the type is viable for the structure?

Types are checked at compile time. You can't realistically do anything about them at runtime, nor would you ever realistically need to. (If you don't want all that type-checking, then why are you using a language that forces you to type in the typenames exclusively all over the place? :) )


I'm not really sure when exactly I should strive for exception safety vs speed

You should always strive for exception safety. Note that "exception safety" doesn't mean, "throwing an exception if anything goes wrong". It means "providing one of the three exception guarantees: weak, strong or nothrow". Throwing exceptions is optional. Exception safety is necessary to allow callers of your code to be satisfied that their code can operate correctly when errors occur.

You will see very different styles from different C++ programmers/teams regarding exceptions. Some use them a lot, others hardly at all (or even strictly not at all, although I think that's fairly rare now. Google is probably the most (in)famous example, check their C++ style guide for their reasons if you're interested. Embedded devices and the innards of games are probably the next most likely places to find examples of people avoiding exceptions entirely in C++). The standard iostreams library lets you set a flag on streams whether they should throw exceptions when I/O errors occur. The default is not to, which comes as a surprise to programmers from almost any other language in which exceptions exist.

Should I really throw an error when a list fails?

It's not "a list" failing, it's specifically pop_front being called when the list is empty that fails. You can't generalize over all operations on a class, that they should always throw exceptions on failure, you have to consider specific cases. In this case you have at least five reasonable options:

  • return a value to indicate whether anything was popped. Caller can do anything they like with this, or ignore it.
  • document that it is undefined behavior to call pop_front when the list is empty, then ignore the possibility in the code for pop_front. It's UB to pop an empty standard container, and some standard library implementations contain no checking code, especially in release builds.
  • document that it's undefined behavior, but do the check anyway, and either abort the program, or throw an exception. You could perhaps do the check only in debug builds (which is what assert is for), in which case you might also have the option of triggering a debugger breakpoint.
  • document that if the list is empty, the call has no effect.
  • document that an exception is thrown if the list is empty.

All of these except the last mean that your function can offer the "nothrow" guarantee. Which one you choose depends what you want your API to look like, and what kind of help you want to give your callers in finding their bugs. Note that throwing an exception does not force your immediate caller to catch it. Exceptions should only be caught by code that's capable of recovering from the error (or optionally at the very top of the program).

Personally, I lean toward not throwing exceptions for user errors, and I also lean toward saying that popping an empty list is a user error. This doesn't mean that in debug mode it isn't useful to have all kinds of checks, just that I don't usually define APIs to guarantee such checks will be performed in all modes.

Is a custom error class really necessary for such a thing

No, it's not necessary, because this is an avoidable error. A caller can always ensure that it won't be thrown, by checking that the list is non-empty before calling pop_front. std::logic_error would be a perfectly reasonable exception to throw. The main reason to use a special exception class is so that callers can catch just that exception: it's up to you whether you think callers will need to do that for a particular case.

Is it possible for head_->prev to ever throw when assigning to 0?

Not unless your program has somehow provoked undefined behavior. So yes, you can decrement the size before that, and you can decrement it before the delete provided you're sure the destructor of ListElem can't throw. And when writing any destructor, you should ensure that it doesn't throw.

I hear often that anything can fail in a C++ program. Is it realistic to test if the constructor for ListElem fails (or tail_ during newing)?

It's not true that everything can fail. Ideally functions should document what exception guarantee they offer, which in turn tells you whether they can throw or not. If they're really well-documented, they'll list everything they can throw, and under what circumstances they throw it.

You shouldn't test whether new fails, you should allow the exception from new, if any, to propagate from your function to your caller. Then you can just document that push_front can throw std::bad_alloc to indicate lack of memory, and perhaps also that it can throw anything that's thrown by the copy constructor of T (nothing, in the case of int). You might not need to document this separately for each function - sometimes a general note covering multiple functions is sufficient. It shouldn't come as a huge surprise to anyone that if a function called push_front can throw, then one of the things it can throw is bad_alloc. It should also come as no surprise to users of a template container than if the contained elements throw exceptions, then those exceptions can be propagated.

Would it ever be necessary to test the type of data (currently a simple typedef int T until I templatize everything) to make sure the type is viable for the structure?

You can probably write your structure such that all is required of T is that it's copy-constructable and assignable. There's no need to add special tests for this - if someone tries to instantiate your template with a type that doesn't support the operations you perform on it, they'll get a compilation error. You should document the requirements, though.


That's a long question. I'll take all the questions that are numbered 1).

1) Should I really throw an error when a list fails? Shouldn't I rather simply do nothing and return instead of forcing the user of the list to perform try {] catch() {} statements (that are also slow).

No. If your user cares about performance they will check the length before attempting to pop rather than popping and catching the exception. The exception is there to inform your user if they forget to check the length first, and at that point you really want the application to blow up in their face. If you just do nothing it could cause subtle problems which only show up later, and that will make debugging more difficult.

1) I hear often that anything can fail in a C++ program. Is it realistic to test if the constructor for ListElem fails (or tail_ during newing)?

The constructor can for example fail if you run out of memory, but in this case it should throw an exception, not return null. So you shouldn't need to test explicitly for the constructor failing. See this question for more details:

  • Will new return NULL in any case?


I hear often that anything can fail in a C++ program. Is it realistic to test if the constructor for ListElem fails (or tail_ during newing)?

Yes, it is realistic. Otherwise, if your program runs out of memory and allocation fails (or the constructor fails for some other internal reason), you will have problems later on.

Basically, you must signal a failure ANY time when the code is unable to fully do something its API declares it will do.

The only difference is how you signal the failure - via return value or via exception. If performance considerations exist, return values may be better than exceptions. But both approaches require special error catching code in the caller.


For your first set of questions:

  1. Yes, you should throw, for all the reasons in @Mark's answer. (+1 to him)
  2. It's not really necessary, but it can make life on your callers a lot easier. One of the benefits of exception handling is that it localizes code to deal with a specific class of error together at one places. By throwing a specific exception type, you allow your callers to specifically catch that error, or be more general about it by catching a superclass of the specific exception you threw.
  3. All of the statements in your else provide the nothrow guarantee.

For your second set:

  1. No, it's not realistic to test. You don't have any idea what the underlying constructor could throw. It could be an expected item (i.e. std::bad_alloc) or it could be something strange (i.e. int), and therefore the only way you could handle it would be to put it inside a catch(...) which is evil :)

    On the other hand, your existing method is already exception safe, so long as the dummy end node created inside the if block will be nuked by the destructor of your linked list. (i.e. everything after the news provides nothrow)

  2. Just assume any operation on T can throw, except the destructor.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号