clear explanation sought: throw() and stack unwinding
I'm not a programmer but have learned a lot watching others. I am writing wrapper classes to simplify things with a really technical API that I'm working with. Its routines return error codes, and I have a function that converts those to strings:
static const char* LibErrString(int errno);
For uniformity I decided to have member of my classes throw an exception when an error开发者_JAVA技巧 is encountered. I created a class:
struct MyExcept : public std::exception {
const char* errstr_;
const char* what() const throw() {return errstr_;}
MyExcept(const char* errstr) : errstr_(errstr) {}
};
Then, in one of my classes:
class Foo {
public:
void bar() {
int err = SomeAPIRoutine(...);
if (err != SUCCESS) throw MyExcept(LibErrString(err));
// otherwise...
}
};
The whole thing works perfectly: if SomeAPIRoutine
returns an error, a try-catch block around the call to Foo::bar
catches a standard exception with the correct error string in what()
.
Then I wanted the member to give more information:
void Foo::bar() {
char adieu[128];
int err = SomeAPIRoutine(...);
if (err != SUCCESS) {
std::strcpy(adieu,"In Foo::bar... ");
std::strcat(adieu,LibErrString(err));
throw MyExcept((const char*)adieu);
}
// otherwise...
}
However, when SomeAPIRoutine
returns an error, the what()
string returned by the exception contains only garbage. It occurred to me that the problem could be due to adieu
going out of scope once the throw
is called. I changed the code by moving adieu
out of the member definition and making it an attribute of the class Foo
. After this, the whole thing worked perfectly: a try-call block around a call to Foo::bar
that catches an exception has the correct (expanded) string in what()
.
Finally, my question: what exactly is popped off the stack (in sequence) when the exception is thrown in the if-block when the stack "unwinds?" As I mentioned above, I'm a mathematician, not a programmer. I could use a really lucid explanation of what goes onto the stack (in sequence) when this C++ gets converted into running machine code.
You are right: exception constructor takes the pointer to the string, it does not store a copy of string content. That content is stored in local variable
char adieu[128];
which goes out of scope upon exit from Foo::bar method.
The stack contains 'activation records' (also called 'stack frames') for functions which are currently executing. Each function call allocates memory on that stack for all local variables declared in that function (at machine level, this may be implemented as 'push' or any other command that advances stack pointer). Each return from the function - it does not matter if it's a normal return or exit by throwing an exception - frees the memory which was allocated when entering the function (at machine level, this is implemented as 'pop' or 'restore stack pointer to the value it had when entering the function').
So, when the stack is unwound, all 'activation records' in 'function call chain' or 'stack' between the function where exception was thrown and the function where the exception is caught are freed.
You have a far easier solution:
struct MyExcept : public std::exception {
std::string errstr;
const char* what() const throw() {return errstr_.c_str();}
MyExcept(int errno, std::string prefix = "") : errstr (prefix + LibErrString(errno)) {}
};
...
throw MyExcept(err, "In Foo::bar... ");
With C strings, you have to worry a lot more about scopes and manual memory managament. You correctly noticed that you exit a few scopes (pop variables and functions off the stack), so C strings are even worse there. C++ strings on the other hand behave much more like integers. Memory management is an integrated feature.
For the same reason, I've moved the LibErrString
call inside your exception class. Error handling code fits naturally in exception classes, and should not clutter business code.
So much for practical code, back to theory. What happens when an exception is thrown and caught? C++ first determines where the exception will be caught. There must be an enclosing try{ }
scope, but possibly there are a lot more scopes in between: function scopes, for-scopes, if-scopes, or just plain block scopes.
These scopes are then exited from the inside out. As each scope is exited, the variables local to that block are destroyed. When a function scope is exited, the next scope to consider is of course the caller; for the other scopes the next scope is the surrounding scope.
You see that variables are destroyed, and functions are exited in a LIFO order. That means that a stack structure is a natural fit. You could also use two stacks, keeping return addresses and variables separate. Or three, to avoid big buffers on the stack. Since there are many reasonable implementations, C++ doesn't actually describe a precise implemenation, just the behavior.
First, your exception class should look like this
class MyExcept : public std::runtime_error {
MyExcept(const std::string & message) : std::runtime_error(message) {}
};
That is really all that is needed. You can change the base-class to std::logic_error
if that suits your needs better (see here for an explanation of the distinction, and here for a list of other predefined exception classes). You can then build the error message using string concatenation:
void Foo::bar() {
int err = SomeAPIRoutine(...);
if (err != SUCCESS) {
throw MyExcept("Error: " + LibErrString(err));
}
// otherwise...
}
This FAQ contains more useful information about exception handling, especially how to correctly catch exceptions:
try {
// fail here
}
catch (std::exception & e) { // NOTE: catch by reference!!!
}
As to your stack question, the current stackframe is unwound, and all objects in it are destroyed in reverse order of their construction. This is done until the exception is caught. This is the reason why RAII works.
精彩评论