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.