Thread: [Alephmodular-devel] On errors
Status: Pre-Alpha
Brought to you by:
brefin
From: Br'fin <br...@ma...> - 2003-01-31 15:22:51
|
In general, I like the design I put forth for IFailable. It's intent was not to cover an object handled by several threads at once. The area I'm having problem is with with CError objects. As I work on it, I'm going lets add this and lets add that. And consequently it seems to be lacking a proper elegance to it. Marathon handles errors like so: The game hits an error and calls set_game_error(type, code) Type may be a gameError or a systemError. If it's a gameError, than code is limited to a particular subset of codes. If it's a systemError, than the particular error is recorded in the code. Code higher up the hierarchy can check if the game has an error_pending, it can read what kind of error is around with get_error and can clear the error. For instance, file finding code might report (systemError, fileNotFound) Further up the code sees this and says: Error pending? Ok, what is it? File not found? Ok, it's just the preferences, clear the error and continue on to create the file. For a contrasting viewpoint on errors, I looked at Java. That has the hierarchy of 'Throwable' leading to 'Exception' (things which your program should catch) and 'Error' (Things which your program shouldn't catch, they're just too big). And then further breakdowns of errors. In our case this would work out to one SystemException and then GameException with a hierarchy of specific exceptions beneath it. Another thing about Java exceptions is that the interface is fairly simple, You create a new exception and the string argument in the creator is optional. Hsm, was just thinking about exceptions and C++. Exceptions get to be a nuisance if the current function itself is just a pass through for an exception. (open_preferences would continue to throw a fileNotFoundException that originated in open_file and also had to propagate through wad_open.) Or do you just need to pussyfoot around your declarations to make sure that if you're passing an exception that you didn't declare any memory before the point where the exception can occur? -Jeremy Parsons |
From: Woody Z. I. <woo...@sb...> - 2003-01-31 16:55:11
|
On Friday, January 31, 2003, at 09:23 AM, Br'fin wrote: > Hsm, was just thinking about exceptions and C++. Exceptions get to be a > nuisance if the current function itself is just a pass through for an > exception. (open_preferences would continue to throw a > fileNotFoundException that originated in open_file and also had to > propagate through wad_open.) Or do you just need to pussyfoot around > your declarations to make sure that if you're passing an exception that > you didn't declare any memory before the point where the exception can > occur? > IIRC C++ is not as picky as Java with respect to declaring 'throws' etc., which could be viewed as a mixed blessing I suppose. As for allocating memory, well your use of the phrase 'declaring memory' concerns me - if a function does int i; or char myStorage[40]; , the memory needed for those operations is taken from the stack, and will be released automatically when the stack is unwound during the exception propagation. OTOH allocating memory with new etc. does take some care, because the exception mechanism (understandably) won't try to follow through the remainder of a function it's unwinding to see if there are any delete's in there. So typically I think C++ programmers would use a stack-based wrapper around dynamically-allocated storage... that is, you have a class that's something like (I'm making this up, I'm sure there are better ways to do these things out there:) template class Autoreleaser<typename tType> { tType* mPointer; public: Autoreleaser(tType* inPointer) { mPointer = inPointer; } ~Autoreleaser() { if(mPointer != NULL) delete mPointer; } }; bool foo() { Blah* theBlah = new Blah; Autoreleaser<Blah> theBlahAutoreleaser(theBlah); // do I have to specify the type explicitly as a template argument? do_some_stuff_that_might_throw(); if(something) return false; do_more_stuff(); return true; } This way (since theBlahAutoreleaser's destructor will be called) theBlah will get deleted when foo() exits either by falling off the end, returning from somewhere, or through exception unwinding. You could always have a method "DontRelease()" or something in Autoreleaser that you could call at the end of your function if you wanted to keep the pointer in a successful operation, etc. I think the C++ Standard Library type 'auto_ptr' might include the above functionality among others. I haven't studied or used it yet. But if it's appropriate, I of course support using the standard mechanism rather than homegrown. My Logging mechanism in A1 is intended for such stack-based use: you use the utility macro logContext1("trying to gather player %s", thePlayerName);, say, and it pushes that context onto the Logger's stack. When the block that 'owns' the stack-allocated variable (hidden from you by the macro) exits, one way or another, that context description is popped off the stack - no explicit action is needed in the code. Log messages emitted with one of the standard Logging macros (logError2("unable to establish connection to %d:%d", theAddress.host, theAddress.port);) thus can be prepended with some or all of their enclosing contexts (but the context switches themselves don't have to be logged if no log messages are emitted within them). The same logging message could be entered from a variety of runtime contexts (e.g. having problems opening a file could happen during startup, or while opening a film, or while trying to load a replacement texture, etc.). The low-level routine logs its problem and depends on the higher-level routines having established a context ("loading replacement texture file '%s' for collection %d shape %d") for that problem. Obviously there's some overhead to switching contexts, even if no intervening messages are logged, but IMO it's a small price for a scheme that's convenient for programmers (and thus will be used) and useful for log-readers (because it provides the information needed without too much excess, and because the programmers actually use it - so it contains (and will contain more) useful information). The existing dprintf() and fdprintf() were forwarded to Logging, so all those existing log messages automatically come out in the new system (and can potentially benefit from context information), giving an immediate 'forced adoption'. ;) Anyway still, reducing the context-switching overhead is one area (of many) for improvement in the scheme. Oops, sorry, did not mean to get _that_ far off track from the original discussion of leveraging destructor calls for stack-based objects. But, I guess I'm me. :) Woody |
From: Dietrich E. <die...@zd...> - 2003-02-01 06:35:31
|
On Friday, January 31, 2003, at 07:23 , Br'fin wrote: > In general, I like the design I put forth for IFailable. It's intent > was not to cover an object handled by several threads at once. > The area I'm having problem is with with CError objects. As I work on > it, I'm going lets add this and lets add that. And consequently it > seems to be lacking a proper elegance to it. > > > Marathon handles errors like so: > > The game hits an error and calls set_game_error(type, code) > > Type may be a gameError or a systemError. If it's a gameError, than > code is limited to a particular subset of codes. If it's a systemError, > than the particular error is recorded in the code. > > Code higher up the hierarchy can check if the game has an > error_pending, it can read what kind of error is around with get_error > and can clear the error. > > > For instance, file finding code might report (systemError, > fileNotFound) Further up the code sees this and says: Error pending? > Ok, what is it? File not found? Ok, it's just the preferences, clear > the error and continue on to create the file. > > > For a contrasting viewpoint on errors, I looked at Java. That has the > hierarchy of 'Throwable' leading to 'Exception' (things which your > program should catch) and 'Error' (Things which your program shouldn't > catch, they're just too big). And then further breakdowns of errors. > > In our case this would work out to one SystemException and then > GameException with a hierarchy of specific exceptions beneath it. Maybe... then maybe the exact circumstances under which an exception is thrown should be better defined first. <thinking-out-loud> Runtime exceptions happen in I/O, system calls, and other juicy things like buffer overflows. Logic errors happen anywhere you shouldn't pass a nil pointer, or something is out of range, etc. </thinking-out-loud> It seems that runtime exceptions should be caught, interpreted, and presented to the user in a friendly manner such as "The shapes file is corrupted." or "The network connection vanished.", whereas logic errors should present themselves as bug reports - "Range error thrown at foo.cpp:273, caught at bar.cpp:450. Please report this to <bug...@bl...>." Logic errors could also be marked as irrecoverable, so after the bug report is presented the game quits. > Another thing about Java exceptions is that the interface is fairly > simple, You create a new exception and the string argument in the > creator is optional. > > > Hsm, was just thinking about exceptions and C++. Exceptions get to be a > nuisance if the current function itself is just a pass through for an > exception. (open_preferences would continue to throw a > fileNotFoundException that originated in open_file and also had to > propagate through wad_open.) Or do you just need to pussyfoot around > your declarations to make sure that if you're passing an exception that > you didn't declare any memory before the point where the exception can > occur? It's not really that weird, just C++ isn't garbage-collected like Java is. You can deallocate in a catch block or by using destructors. The auto_ptr's will free their pointees automagically. On Thursday, January 30, 2003, at 04:39 , Br'fin wrote: > My first instinct is 'what on earth do we have for objects that are > accessed in multiple threads *AND* which are failable'. > > As for existing error handling, I was looking at game_errors.h/.cpp and > some of how iostream type stuff does it. In the case of an iostream, > the stream itself does get into bad states (The object is affected) Well, yah. File handles have states, so there is no possible situation in which error is independent of state. But other objects aren't like that, and multiple error-handling systems would be ugly. > A quick perusal of set_game_error (Current error handling) reveals the > majority are around file handling (basic, wad, and preferences) and a > couple netgame spots. (Inability to sync with others or server died) > > In the case of Marathon, the game itself goes into a bad state until > falling far enough back in its main thread to correct it and/or report > to the user. This would be a heck of a lot cleaner without explicit error checking, as exceptions provide. I think I'd enjoy these concepts a whole lot more if I didn't absolutely hate C++. |
From: Br'fin <br...@ma...> - 2003-02-01 16:20:29
|
Mostly still thinking out-loud myself. Mostly with the errors I want to keep things simple and sweet. While the existing error handling isn't anything much to speak of in terms of threading or such, it is simple and sweet. There's no convoluted class hierarchy, there's no tons of arguments. And C++ exceptions almost seem like they come with more baggage than they solve. Grrf -Jeremy Parsons |
From: Br'fin <br...@ma...> - 2003-02-01 19:38:45
|
And still more thinking on errors, but more details too, I'm liking the approach below: /////////////////////////////////////////////////////////////////////// // // $Id: CErrror.h $ /////////////////////////////////////////////////////////////////////// // /* * CErrror.h * AlephModular * * CError defines an interface for classes that can error an go into a * bad state. For instance, opening a file fails so you need to report * this back to the calling function. * * The basic method this supports is the following: * A class has a method this_may_fail that returns a boolean (If the * method needs to return something meaningful like an opened file * reference, then this is done with out arguments passed by * reference) If the method is successful, then it returns true. * Otherwise it returns false and puts the class into a failed state. * You may get a class's current state to report why an operation * failed. * * Created by Br'fin on Thu Jan 30 2003. * */ #ifndef __CERROR_H #define __CERROR_H #include "cseries.h" #include <string> class CError; /* * IFailable * Interface * * This is an interface to be supported by all classes that may fail. * For instance, opening a file may fail. * */ class IFailable { protected: /* * set_error * arguments: * an explanatory member of CError that explains the current error */ virtual void set_error(const CError &) = 0; public: /* * is_good * returns: * true if and only if there is no error state on the object */ virtual bool is_good() const = 0; /* * get_error * arguments: * returns the objects's current error state. */ virtual void get_error(const CError &) const = 0; /* * clear_error * This attempts to clear the error state of the object. This * is not guarunteed to succeed. * returns: * true if the error can be cleared. False otherwise */ virtual bool clear_error() = 0; virtual ~IFailable() {} // Required virtual destructor }; /* * -Jeremy * We could go with all sort of subclasses and introspection and details. * However, that gets too convoluted for our needs. Like hitting a nail * with a pile driver. And doesn't sit right with me. * * CError describes an error that occured in one part of the system. * * If the error occured in some part of the operating system, then the * error's type should be systemError and the err_code can be an OS * error code. But should conform to an agreed upon external code. * For instance a file not found value. * * On the other hand, a gameError is tied to a state that AM must * report to the user. And in many cases, one system turning up * a systemError will have that error inspire a gameError to * display to the user. * * Why this disjoint? Well the gui should be familiar with what * gameErrors there are, and have access to appropriate resources * and localization information to display them to the user. * * So, what is a CError exactly? * It is a type of error with a given code. As explained above. And * allows a string of details to be provided. There is no gauruntee * that the details will be displayed. */ class CError { public: typedef enum { /* types */ systemError, gameError } type; typedef enum { /* Game Errors */ errNone= 0, errMapFileNotSet, errIndexOutOfRange, errTooManyOpenFiles, errUnknownWadVersion, errWadIndexOutOfRange, errServerDied, errUnsyncOnLevelChange, NUMBER_OF_GAME_ERRORS } game_errors; private: type err_type; int32 err_code; std::string details; public: CError(type _type, int32 _code, std::string _details = "") : err_type(_type), err_code(_code), details(_details) { #ifdef DEBUG if(err_type==gameError) assert(err_code>=0 && err_code<NUMBER_OF_GAME_ERRORS); #endif } CError(const CError& err) : err_type(err.err_type), err_code(err.err_code), details(err.details) {} CError& operator=(const CError err) { err_type = err.err_type; err_code = err.err_code; details = err.details; return *this; } type get_type() { return err_type; } int32 get_code() { return err_code; } void get_details(std::string& _string) { _string = details; } bool has_details() { return details.length() > 0; } }; #endif |