可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I am developing a C++ dongle communication library. The library would provide an unified interface to communicate with a range of remote code execution dongles like SenseLock, KEYLOK, Guardant Code.
The dongles are based on a smart card technology and have an internal file system and RAM.
A typical operation routine involves (1) enumeration of dongles connected to the USB ports, (2) connection to a dongle selected, (3) execution of the named module passing input and collecting output data.
Well, it is trivial that all of these stages can end up with an error. There can be many cases, but the most general are:
- A dongle is not found (sure a fatal case).
- A dongle connection failure (a fatal case).
- The execution module specified is not found within the dongle (?).
- The operation requested failed due to timeout (?).
- The operation requested needs authorization (a recoverable case I suppose).
- A memory error occurred while executing a module in a dongle (sure a fatal case).
- A file system error occurred in a dongle (sure a fatal case).
? - I don't know yet if the case is considered fatal or not.
I am still deciding whether to throw exceptions, to return an error codes, or to implement a methods for both cases.
The questions are:
- Do exceptions replace the error codes completely or maybe I need to use them only for "fatal cases"?
- Is mixing two paradigms (exceptions and error codes) considered a good idea?
- Is it good idea to provide user with two conceptions?
- Are there any good examples of the exceptions and error codes mixing conception?
- How would you implement this?
Update 1.
It would be interesting to see more opinions from different perspectives, so I decided to add a 100 reputation bounty to the question.
回答1:
If we are talking about C++ application/module internal error-handling policy then my personal opinion is:
Q: Do exceptions replace the error codes completely or maybe I need to use them only for "fatal cases"?
A: They do. Exceptions are always better for C++ function than returning error codes. The reason is explained below.
Q: Is mixing two paradigms (exceptions and error codes) considered a good idea?
A: No. Mixing is ugly and makes error handling inconsistent. When I'm forced to use error-returning API (like Win32 API, POSIX, etc) I use exception throwing wrappers.
Q: Is it good idea to provide user with two conceptions?
A: No. Users are confused which variant to choose and usually make worst decision of mixing both. Some users prefer exceptions others prefer error-returning and if all of them work on the same project they make project's error-handling practice a total mess.
Q: Are there any good examples of the exceptions and error codes mixing conception?
A: No. Show me if you find one. IMO isolating error returning functions with exception throwing wrappers is the best practice if you have to use error-returning functions (and you usually do have to use them).
Q: How would you implement this?
A: I would use exceptions only. My way is returning only in the case of success. Error-returning practice heavily messes the code with error-checking branches or even worse - error status checking is missing and thus error status is just ignored that make the code full of hidden bugs that hard to reveal. Exceptions make error handling isolated. If you need to handle some kind of error in-place it usually means that it is not an error at all but just some legitimate event that may be reported by successful return with some specific status indicated (by return value or otherwise). If you really need to check if some error occurred locally (not from the root try/catch block) you can try/catch locally so using only exceptions doesn't really limit your capabilities in any way.
Important Note:
For every particular situation it is very important to correctly define what is error and what is not (for best usability).
E.g. say we have a function that shows input dialog and return text entered by the user and if the user may cancel the input then cancel event is success - not error (but it must be somehow indicated on return that user canceled the input) but lack of resources (like memory or GDI objects or something) or something like the absence of the monitor to show the dialog is indeed error.
In general:
Exceptions are more natural error-handling mechanism for C++ language. So using exceptions is a good idea if you are developing C++ application or library to be used by C++ application only (not by C application, etc). Error-returning is more portable approach - you may return error codes to applications written on any programming language and even running on different computer. Of course nearly always OS requests report their status via error-codes (because it is natural to make them language-independent). And for that reason you have to deal with error-codes in every-day programming. BUT IMO planning error-handling policy of C++ application to be based on error-codes is just asking for trouble - the application becomes a totally unreadable mess. IMO the best way to deal with status codes in C++ application is using C++ wrapper functions/classes/methods to call error-returning functionality and if error is returned - throw exception (with status info embedded into exception class).
Some important notes and caveats:
In order to use exceptions as error-handling policy in a project (either big one or small one) it is important to have a strict policy of writing exception safe code. It basically means that every resource is acquired in constructor of some class and more importantly released in destructor (that will make sure you don't have resource leaks). And also you have to catch exceptions somewhere - usually in your root-level function (like main
or window procedure or thread procedure, etc).
Consider this code:
SomeType* p = new SomeType;
some_list.push_back(p);
/* later element of some_list have to be delete-ed
after removing them from this list */
It is typical potential memory leak - if push_back throws an exception then dynamically allocated and constructed SomeType object is leaked.
Exception safe variant is this:
std::auto_ptr<SomeType> pa( new SomeType );
some_list.push_back(pa.get());
pa.release();
/* later elements of some_list have to be delete-ed
after removing them from this list */
Or:
boost::shared_ptr<SomeType> pa( new SomeType );
some_list.push_back(pa);
/* some_list is list of boost::shared_ptr<SomeType>
so everything is delete-ed automatically */
If you are using C++ standard templates, allocators, etc. you either write exception safe code (if you try/catch every single STL call the code becomes a mess) or leave the code full of potential resource leaks (that is unfortunately happen very often). True C++ application must be exception safe.
回答2:
Are there any good examples of the exceptions and error codes mixing conception?
Yes, boost.asio
is the ubiquitious library used for network and serial communication in C++, and nearly every function comes in two versions: exception-throwing and error-returning.
For example, iterator resolve(const query&)
throws boost::system::system_error
on failure, while iterator resolve(const query&, boost::system::error_code & ec)
modifies the reference argument ec
.
Of course what is good design for a library, is not a good design for an application: the application would do best to use one approach consistently. You're creating a library, though, so if you're up for it, using boost.asio as a model could be a workable idea.
回答3:
- Use error codes where the application would typically continue execution from that point.
- Use exceptions where the application would typically not continue execution from that point.
I actually mix error codes and exceptions from time to time. Contrary to some other answers, I don't think this is "ugly" or bad design. Sometimes, it's inconvenient to have a function throw an exception upon error. Suppose you don't care if it fails:
DeleteFile(filename);
Sometimes I don't care if it fails (e.g. with a "file not found" error) - I just want to make sure it's deleted. This way I can ignore the returned error code without having to put a try-catch around it.
On the other hand:
CreateDirectory(path);
If this fails, the code which follows is probably also going to fail, so the function should not continue. Here it's convenient to throw an exception. The caller, or somewhere further up the call stack, can figure out what to do.
So, just think about whether it is likely the code following it is going to make any sense if the function failed. I don't think mixing the two is the end of the world - everyone knows how to deal with both.
回答4:
Exceptions are good when the code supposed to handle the error is far away (many layers up) from the site detecting the problem.
Status codes are good if negative status is expected to be returned "often" and if the code calling your's are supposed to take care of that "problem".
Naturally, there is a large gray zone here. What is often, and what is far away?
I wouldn't recommend you to provide both alternatives, as that is mostly confusing.
回答5:
I must admit I appreciate the way you categorize the errors.
Most people will say that exceptions should cover exceptional cases, I prefer to translate to user-land though: unrecoverable. When something happens that you know your user cannot easily recover from, then throw an exception, this way he won't have to handle it each time it calls you but will just let it bubble up to the top of its system where it'll be logged.
The rest of the time, I go for using types that embed errors.
The simplest is the "optional" syntax. If you are looking for an object in a collection, then you might not find it. There is a single cause here: it's not in the collection. As such, error code are definitely spurious. Instead one can use:
- a pointer (shared if you want to share ownership)
- a pointer-like object (like an iterator)
- a value-like nullable object (kudos to
boost::optional<>
here)
When things are more tricky, I tend to use an "alternative" type. It's the idea of the Either
type in haskell, really. Either you return what the user asked for, Or you return an indication of why you didn't return it. boost::variant<>
and the accompanying boost::static_visitor<>
play nicely here (in functional programming it's done through object deconstruction in pattern matching).
The main idea is that error code can be ignored, however if you return an object which is both the result of the function XOR the error code, then it cannot be silently dropped (boost::variant<>
and boost::optional<>
are really great here).
回答6:
There is a major point here to consider, this is for a locking mechanism, giving out error codes as to the details of what fails is like telling a lock picker that the first 3 of the 4 pins inside the lock he has correct.
You should omit as much information here as possible, and make sure that your check routine ALWAYS takes the same amount of time to verify the card so that a timing attack is not possible.
But back to the original question, in general I prefer an exception to be raised in all cases so the calling application can decide how it wants to handle them by wrapping the call into a try/except block.
回答7:
The way I do this is I have an Exception class ( just the exception object you throw ) which consists of a string message and an error code enum and some nifty constructors.
I only wrap things in a try catch block when recovery is meaningful and possible. Commonly that block will attempt to handle only one or two of the enumerated error codes while rethrowing the rest. At the top level my entire app runs in a try catch block that logs all unhandled non fatal errors, while it exits with a message box if the error is fatal.
You could alternatively have a seperate Exception class for each kind of error, but I like to have it all in one place.
回答8:
Do exceptions replace the error codes completely or maybe I need to use them only for "fatal cases"?
reserve them for what's truly necessary. if you write it this way from the outset, there will be very few you'll need to handle/pass. keep the issues local.
Is mixing two paradigms (exceptions and error codes) considered a good idea?
i think you should base your implementation off error codes, and use exceptions in truly exceptional situations (e.g. no memory, or that you must catch one thrown to you). otherwise, prefer error codes.
Is it good idea to provide user with two conceptions?
no. not everyone uses exceptions, and they are not guaranteed to safely cross boundaries. exposing them or throwing them into the client's domain is a bad idea, and makes it difficult for clients to manage apis you provide. they will have to handle multiple exits (result codes and exceptions) and some will have to wrap your interfaces in order to use the library in their codebase. error codes (or something similar) are the lowest common denominator here.
How would you implement this?
i would not subject clients to exceptions, it's more to maintain and can be insulated. i'd use error codes or a simple type which provides additional fields where needed (e.g. a string field if you would supply a recovery suggestion for the end user). and i'd try to keep it quite minimal. also, provide a means for them to test with increased diagnostics for development.
回答9:
I'm not sold on how good an idea this is, but recently I worked on a project where they couldn't toss around exceptions, but they didn't trust error codes. So they return Error<T>
, where T is whatever type of error code they would have returned (usually and int of some sort, sometimes a string). If the result went out of scope without being checked, and there was an error, an exception would get thrown. So if you knew there was nothing you could do, you can ignore the error and generate an exception as expected, but if you could do something then you can explicitly check the result.
It was an interesting mixture.
This question keeps popping to the top of the active list, so I figure I'll expand a bit on how this worked. The Error<T>
class stored a type-erased exception, so its use didn't force the use of a specific exception hierarchy or anything like that, and each individual method could throw however many exceptions as it liked. You could even throw int
s or whatever; pretty much anything with a copy constructor.
You did lose the ability to break on an exception being thrown, and wind up at the source of the error. However, because the actual exception is what ends up thrown [its just the location that changed], if your custom exception creates a stack trace and saves it on creation, that stack trace will still be valid whenever you get around to catching it.
One big issue that could be a real game breaker, is that the exception is thrown from within its own destructor. Which meant you ran the risk of it resulting in your application terminating. Oops.
Error<int> result = some_function();
do_something_independant_of_some_function();
if(result)
do_something_else_only_if_some_function_succeeds();
The if(result)
check ensures that the error system lists that error as handled, so there is no reason for result
to throw its stored exception on destruction. But if do_something_independant_of_some_function
throws, result
will get destroyed before reaching that check. This results in the destructor throwing a second exception, and the program just gives up and goes home. This is extremely easy to avoid [always check the result of a function before doing anything else], but still risky.
回答10:
Do exceptions replace the error codes completely or maybe I need to use them only for "fatal cases"?
Exceptions do not replace error codes universally. There are many low-level functions that have several return values that may or may not be considered errors depending on the user's context. For example, many I/O operations can return these responses:
EINTR
: operation interrupted, sometimes it's worth restarting the operation and other times it means "user wants to exit the program"
EWOULDBLOCK
: operation is non-blocking, but no data can be transferred now. If a caller expects reliable nonblocking behavior (e.g. UNIX domain sockets), this is a fatal error. If a caller is checking opportunistically, this is passive success.
- partial success: In a streaming context (e.g. TCP, disk I/O), partial reads/writes are expected. In a discrete messaging context (e.g. UDP), partial reads/writes may indicate fatal truncation.
It's clear when you're working at this level that exceptions aren't appropriate because the author of the implementation cannot predict which "failure responses" will actually be considered critical failures by a given user, or just normal behavior.
回答11:
Keep exceptions local to your library/middleware, for your logic.
Use error codes instead to communicate with the client.