한글 버전은 왜 예외를 쓰는 게 좋을까요?링크를 따라 가시면 됩니다.
Are you using C++ exceptions when you want to indicate an error in a subroutine? If not, why? Your answer may be that you can return a special error code indicating the error, so you totally don't need to use it. If so, why? Your answer may be what else possible?
This page is written to reveal why exceptions have been introduced in C++. Through this explanation, two kinds of people above may gain firmer understanding on exceptions, which will help them to write better codes using exceptions.
Why do you think exceptions have been introduced in C++ ? Why do you think modern languages like Java and C# still inherit the legacy from C++ ? There are definitely some reasons for that. Here is the why. To implement fault-tolerant softwares more easily.
The most difficult challenge to achieve it on programming languages with states like C and C++ is that softwares themselves should be able to be in a consistent state after error handling.
For examples, suppose the C code snippets which try to open a file and read data from it as follows
char* OpenAndRead(char* fn, char* mode) { FILE* fp = fopen(fn, mode); char *buf = malloc(BUF_SZ); fgets(buf, BUF_SZ, fp); return buf; }
The objective of OpenAndRead() is pretty simple and straight forward. So the implementation of OpenAndRead() is very very simple and easy to read and understand. The above code works well if every subroutine goes smoothly. But what if 'fn' file does not exist or the access is not permissible for the file? What if malloc() or fgets() fails? To make the above code fault-tolerant, we need to check if an error occurs after each call to a subroutine and decide what actions to be taken to recover from the error. Here we assume that reactions to errors are simply to print error messages and release acquired resources and return -1. The code may be modified as follows
int OpenAndRead(char* fn, char* mode, char** data) { *data = NULL; char* buf = NULL; FILE* fp = fopen(fn, mode); if (!fp) { printf("Can't open %s\n", fn); return -1; } buf = malloc(BUF_SZ); if (!buf) { printf("Can't allocate memory for data\n"); fclose(fp); return -1; } if (!fgets(buf, BUF_SZ, fp)) { printf("Can't read data from %s\n", fn); free(buf); fclose(fp); return -1; } *data = buf; return 0; }
What do you feel when comparing two code snippets? The second version becomes harder to read and understand because the main algorithm is mixed with error handling. It even requires much labors because LOC(Line Of Code) becomes much larger!
Exceptions can solve this problem. Let's assume fopen(), malloc(), and fgets() trigger exceptions such as FileDoesNotExist, FileNotAccessible, MemoryExhausted, EndOfFile respectively. Then the above code may be transformed into the following
int OpenAndRead(char* fn, char* mode, char** data) { *data = NULL; char* buf = NULL; FILE* fp = NULL; try { fp = fopen(fn, mode); buf = malloc(BUF_SZ); fgets(buf, BUF_SZ, fp); *data = buf; return 0; } catch (const FileDoesNotExist&) { printf("Can't open %s\n", fn); } catch (const FileNotAccessible&) { printf("The access %s is not permissible for %s\n", mode, fn); } catch (const MemoryExhausted&) { printf("Can't allocate memory for data\n"); fclose(fp); } catch (const EndOfFile&) { printf("Can't read data from %s\n", fn); fclose(fp); free(buf); } return -1; }
Do you see now the main control flow of the OpenAndRead() is clearly seperated from the error handling ? This version of OpenAndRead() becomes more readable. But it still requires much LOC. But, in fact, you can have same effects with less code if you know what happens when an exception occurs, called "stack unwinding"
When an exception is thrown and the stack is unwound until an exception handler is found that is prepared to handle (catch) the exception, the C++ run time calls destructors for all automatic objects constructed since the beginning of the try block. This process is called stack unwinding. The automatic objects are destroyed in reverse order of their construction.
So, if you declare the resource holders as automatic objects, then those objects is cleaned up automatically even when an exception is thrown. Those resource holder classes are out there for file and memory resources. You can use std::fstream for file resource and boost::scoped_ptr for memory resource. Finally, the OpenAndRead() can be implemented as follows
char* OpenAndRead(const std::string& fn, std::ios_base::openmode mode) { try { std::fstream fs(fn.c_str(), mode); boost::scoped_ptr<char> buf(new char[BUF_SZ+1]); fs.getline(buf.get(), BUF_SZ); return buf.get(); } catch (const std::ios_base::failure&) { std::cout << "Can't open or read data from " << fn << std::endl; } catch (const std::bad_alloc&) { std::cout << "Can't allocate memory for data" << std::endl; } return 0; }
All the resource clean-ups are taken care of by C++ run-time. So you don't need to add those code to error handling. The OpenAndRead() becomes quite simple and readable.
When an exception occurs, the C++ run-time will unwind the stack until an exception handler for it while calling the destructors in the reverse order. In this case we have handlers at the same stack frame and we have two automatic objects, say, fs & buf. Before the control is transfered to a handler, destructors for both fs & buf will be called and they will release internal resources.
I can easily guess that you don't feel satisfactory with the above because reactions to exceptions are too simple. With the above code, the caller of OpenAndRead() can just get null pointer or normal pointer which can just indicate failure or success respectively. In the caller's point of view, it can't do anything with the returned null pointer if it does not know why a failure happened.
As said above, another difficult challenge for fault-tolerant softwares is to decide how to react to errors. Important informations for choosing reactions to an error are the context the error occurs in and the reason the error occurs. But unfortunately it is typical that informations are scattered here and there. We know that two parts of codes are involved in error handling. One is callee, the other is caller. It is highly likely that callee knows why an error occurs but not the context. On the contrary, it is highly likely that caller knows the context but not why. Then, what can we do with this situation?
Obviously one solution to this problem is that callee provides as much accurate reason as possible to caller with enough context. But what if a caller is not a direct caller of a callee? It is very common that a caller, which can decide reactions to an error with enough contexts, is far from a callee and even if a callee which detects an error reports exactly the reason why the error occurs but intermediate function between the callee and the caller may ignore details and just returns an indication for success or failure. Let's see concrete example for this.
int OpenAndRead(char* fn, char* mode, char** data) { *data = NULL; char* buf = NULL; FILE* fp = fopen(fn, mode); if (!fp) { printf("Can't open %s\n", fn); return -1; } buf = malloc(BUF_SZ); if (!buf) { printf("Can't allocate memory for data\n"); fclose(fp); return -2; } if (!fgets(buf, BUF_SZ, fp)) { printf("Can't read data from %s\n", fn); free(buf); fclose(fp); return -3; } *data = buf; return 0; } char* foo(char* fn, char* mode) { char* data = 0; if (OpenAndRead(fn, mode, &data) >= 0) return data; else return 0; } void bar() { ...... // get fn and mode char* data = foo(fn, mode); if (data) { ...... // normal processing } else { ...... // what can bar() do? } }
OpenAndRead() tries to differentiate one error from the other by return code but foo() ignores it and just returns null pointer or normal pointer. So, bar() doesn't have many options with null pointer because it can't find out what is really going on. Possible options for bar() may be either to print and exit or to ignore it if possible. This situation gets worse without exceptions because each function on a call tree is enforced to be involved in error processing in some way or another. But an author of some function may not know what a returned error code really means, doesn't care what it means, or he(she) just can't decide what action should be taken because it doesn't know contexts. So, it is very common that at some function on a call tree, a returned error code turns into just a failure indication.
This problem can be addressed with exceptions. If a function on a call tree can't handle an exception, it may just not catch the exception and just ignore it. It can handle exceptions which it can really handle.
Let's finally transform OpenAndRead() once more. Let's suppose here both OpenAndRead() and foo() don't know how to exactly handle exceptions but bar() does.
char* OpenAndRead(const std::string& fn, std::ios_base::openmode mode) { std::fstream fs(fn.c_str(), mode); // std::ios_base::failure() may occur boost::scoped_ptr<char> buf(new char[BUF_SZ+1]); // std::bad_alloc() may occur fs.getline(buf.get(), BUF_SZ); return buf.get(); } char* foo(char* fn, char* mode) { // just not to catch any exception return OpenAndRead(fn, mode); } void bar() { ...... // get fn and mode try { char* data = foo(fn, mode); } catch (const std::ios_base::failure&) { std::cout << "Can't open or read data from " << fn << std::endl; // You can print more precise diagnostic message if you know why you try to open and read fn // and inform a user to take some action here } catch (const std::bad_alloc&) { std::cout << "Can't allocate memory for data" << std::endl; // You can handle bad_alloc exception more gracefully here // if you have reserved memory for this situation } }
As you can see in the above code, OpenAndRead() and foo() become pretty simple and only bar() is involed in error handling and it can handle exceptions really well because it has enough context information and know what exceptions really mean.
One more good thing with exceptions is that an exception is an object, which means that you can encode as much information as possible in an exception and throw it. In other words, an exception is much more expressive than a return code. Then exception handler can access that information and it'll be very helpful for exception handler to find out what is really going on.
Now, I ask you "wanna use C++ exceptions?" What do you say? I hope your answer be "why not?"






Historically, Ada used this kind of concept while CLU was the first one introduced it.
CLU has had too simple syntax but was worthy to be honored as a creative concept of "Exception handling".
Ada has added easier and helpful concept of Exception Handling which C++ got influenced, later.
Here is an interesting story.
I've learned that Ada was required to implement exception handling when researchers designed this language since it was developed for military use. They need to use some computers in any hazardous environment like inside a tank or fighter on battlefield. So, nobody knows what happens during run-time although the programming was perfect (this is impossible, anyway).
Later on, US Dept. of Defense required all military projects to use Ada language when there's more than 30% of new code because of that. It's also been propagated to other NATO countries.
Interestingly, PL/1 also has had exception handling but I don't remember the syntax. I only know it was burden to compiler to optimize codes.
Thanks for you comment. It's the first time to read US Dept. of Defense history.
Post preview:
Close preview