Not ruling out exceptions means writing exception-safe code, which requires some additional care, but is not difficult once you get the hang of it. It also means that you can't rely on taking advantage of compiler options that might generate code that is slightly more efficient than exception-capable code. These costs are almost always worth the benefits in the long run.
You need to be careful about what you call a bug in the client code. Defining every case that you don't want to deal with as a "caller error" leads to poor runtime efficiency and poor code economy, because it burdens all of a module's clients. For example, a queue class should either allow pop() when it is empty, provide an empty detection method, or both.
Error codes have the advantage of being efficiently propagated. However, they also have the disadvantage of requiring the immediate caller to handle them, which makes the code more difficult to maintain, especially when the callee then needs to return a struct because it already had a return value. Furthermore, if the caller doesn't handle the error itself (possibly by converting it to an exception), then the caller's caller also needs to deal with an error code, and so on.
Exceptions have the advantage of being automatically propagated to the nearest outer scope that is prepared to deal with them, without requiring any additional program code to direct this propagation. However, the "stack unwinding" code that is executed when an exception actually occurs takes a long time to execute (typically much longer than the overhead of an ordinary function call).
As a rule, the stack unwinding overhead should not exceed 10% of the available execution time under typical conditions, and should not exceed 50% of the available execution time under any conceivable conditions. If that isn't the case, then you should be using error codes rather than exceptions in the areas that account for the greatest portion of the overhead. (We generally assume that efficiency is an important concern in C++ programming, because otherwise you could save yourself a lot of trouble by using Java or Perl instead.)
It is often the case that a method cannot tell whether error codes or exceptions are more appropriate for each of its clients. In those cases, it is recommended that two methods be provided: one that returns an error code, and a second one that wraps the first, converting any errors into exceptions.
If an error condition is propagated to the caller, then regardless of whether it is propagated via error codes or exceptions, in most cases you'll want to satisfy the Strong guarantee. The Strong guarantee dictates that if an error or exception escapes the call, then the call has no visible side effects (other than the possible logging of events, provided that the fact that such events are reverted due to an exception is also logged). More precisely, if an error or exception escapes a function satisfying the Strong guarantee, then any subsequent behavior of the program (disregarding performance, logging, execution statistics, memory fragmentation, etc.) satisfies all guarantees that would have held had the function call been replaced by a corresponding error code value or throw statement.
Avoid non-const member functions that return an object by value, because they make it impossible for the caller to satisfy the Strong guarantee. See GOTW #8 . Instead split it into two different methods, or assign the result through a reference parameter. (Presumably, if return by reference were suitable, then you'd already be doing that.)
There are cases in which the Strong guarantee is impossible, or its runtime and memory costs are impractical. In particular, general purpose code (such as container classes) that might be used in such a case must violate the Strong guarantee, in which case such code may be wrapped by code that causes it to be satisfied.
In any case, it should always be clear which guarantee holds for a given call. A reasonable convention is to assume the Strong guarantee unless otherwise noted.
For example, a method that aborts on a particular error might be expected to satisfy the NoThrow guarantee. If you renege on that by throwing an exception on the error, then you jeopardize all of your module's clients, which is generally unacceptable. Instead, you can provide a new method that converts the error to an exception, but you'll still need to change, possily at some considerable expense, all of the client code that wants to take advantage of this new method.
As with all other aspects of program design, you are well advised to spend a lot of time up front to consider all the implications of the design, including gross assessments of efficiency, before detailed coding. This will minimize the need for expensive late redesign.
Anders Johnson, last modified $Date: 2003/01/08 $