Thread: So... handling errors.

  1. #1
    (?<!re)tired Mario F.'s Avatar
    Join Date
    May 2006
    Location
    Ireland
    Posts
    8,446

    So... handling errors.

    Ok. It came the time I have to brace myself and start delineating some error handling strategy for my pet project. I'm probably doing this late already since the usual advise is "delineate a rational error handling strategy early in your project and stick to it."

    But I understand the operative word in the above quote is rational. And that is what scares me. Everyone I read so far are very clear that there is no clear way to deal with this. Freedom can be a very scary thing, and it doesn't mean we can't go wrong. Probably the best quote I've found so far is "There are two ways to write error-free programs; only the third one works".

    So, I won't be asking for specific advise. I understand you can't give it unless I can give a specific example. Which unfortunately for most cases, I can't.

    Anyways, the tools I'm going to use are:
    * C++ Exceptions,
    * Assert,
    * Boost::static_assert (compile-time assertions)
    * and of course... avoiding errors.

    Some notions:
    - My project is OOP oriented. Everything is an object. Objects have relations.
    - Classes responsibility includes managing themselves.
    - Some preconditions, postconditions and invariants are dependant on the programmer, others on user input.

    And here goes my first doubt (more to come):
    One of the difficulties I'm having is understanding exactly when and how to use exceptions. Here's an example, some classes are instantiated from dat files. If the file is non existing or its data is corrupt this is a runtime error of which there is no solution. The program must abort. Should I throw an exception from within the class even considering that the only way to handle is to abort? And how do I abort gracefully from within a class definition making sure all my resources are cleaned?

    Currently what I have is something like this:

    Code:
    // CObject.hpp
    
    class objects_file_error : public std::runtime_error {
    public:
        objects_file_error(): std::runtime_error("obj.dat file is missing or corrupt.") {}
    };
    
    class CObject {
    // inlining it just to cut down on typing
    virtual bool load(const std::string& id) try {
        /*...*/
    } catch (objects_file_error& err) {
        /*... here I output the error message ... */
        throw;
    }
    };
    Is this correct?

    And what about moving user-defined exception classes to their own header file and create some hierarchy from there?
    Originally Posted by brewbuck:
    Reimplementing a large system in another language to get a 25% performance boost is nonsense. It would be cheaper to just get a computer which is 25% faster.

  2. #2
    (?<!re)tired Mario F.'s Avatar
    Join Date
    May 2006
    Location
    Ireland
    Posts
    8,446
    I'm sorry for bumping this. But it got moved way down when the forums date was fixed.
    Originally Posted by brewbuck:
    Reimplementing a large system in another language to get a 25% performance boost is nonsense. It would be cheaper to just get a computer which is 25% faster.

  3. #3
    Registered User
    Join Date
    Jan 2005
    Posts
    7,366
    Since the dat file should always be there, and it is a fatal error if it is not, I think an exception is the right choice. This is in contrast to a user specified work file that may or may not exist, which might not warrant an exception since it can happen so easily in a working program.

    Since the error is fatal, I would let the exception propagate all the way to one of the highest levels of your program. If you don't have any way of handling it other than putting up an error message and aborting, then higher level is usually better since at that place you can control cleanup and the exiting of the program. If you catch it deeper into your call stack, then you won't know as well how to cleanup.

    If, in the future, you find a way to handle this error, you can always add a try-catch block at the location in the code that can handle it. Then you can make your attempts to recover. If they succeed you can continue, if they fail you can re-throw the exception and allow your original error message and abort code to exit the program.

    Attempt to minimize the use of try-catch blocks to locations that can truly handle the exception.

  4. #4
    Registered User
    Join Date
    Jun 2005
    Posts
    6,815
    The reason there is no fixed answer, and there is freedom in what you can do, is that the best error handling strategy depends on what you're doing.

    Personally, I usually consider there are four basic aspects of errors;

    1) Cause;
    2) Detection;
    3) Reporting;
    4) Recovery

    In a non-trivial system, the cause might be an emergent property and it may not be possible to determine that the result of some action will -- eventually -- cause an error. Detection is the point where sufficient information comes together, and there is a deliberate step to recognise the error condition. Reporting is the means by when information about the error is propagated from the detection point. Recovery is what happens at various places in response to an error being reported.

    Now, let's characterise your tools.

    C++ exceptions are a runtime means of reporting an error. The intent is that the error report cannot be ignored. The try/catch block is the means by which some function registers interest in a particular bit of code, and implements recovery. Without a suitable try/catch block, the program will exit (which is a negative form of recovery, as it ensures the program does not cause other problems). Exceptions also ensure that cleanup occurs (eg as the stack unwinds, local objects are destructed). Hence the opportunities for using exceptions is when there is a need to recover from an error and a hope that the error can be recovered from or a need for cleanup to occur when the error occurs.

    Assert is another runtime error detection mechanism, which results in program termination without cleanup. It is best suited to situations where there is no hope that an error can be recovered from, or if an attempt at recovery can cause further problems. One problem with assertions is that they tend to upset the end-user the most: there is the highest likelihood of a program aborting without necessary cleanup (eg saving of files).

    Compile time assertions are a means in which the recovery mechanism is failure to create an executable, and forcing the programmer to fix the error. The problem with compile time assertions is that it is necessary for all information to be available to the compiler, which means it is not possible to detect errors caused by input data or user actions. The advantage is that they force the programmer to avoid the error in the first place, without placing an embuggerance on the end user.

    Avoiding errors is self explanatory: if no error occurs, it is not necessary to report or recover from it. The catch with avoiding errors is that it is not always possible. For example, it is not possible to ensure the user behaves as intended.

    Another tool is error codes: return an error status. This is a different reporting mechanism. The strength is that error codes need not be examined, so error codes are appropriate if recovery is not mandatory. The weaknessis that error codes need not be examined, which means that errors can be unintentionally ignored.

    The basic message from the above is that all errors are detected in different means, and reporting/recovery means depend on what the error means to the system. If there are several error types, multiple error handling means may be appropriate.

  5. #5
    Registered User
    Join Date
    May 2003
    Posts
    1,619
    For any exception that should completely terminate the program without any hope of recovery, just let it propagate all the way back to main(), pop up an error message in the catch(), and return an error code (e.g. anything nonzero).
    You ever try a pink golf ball, Wally? Why, the wind shear on a pink ball alone can take the head clean off a 90 pound midget at 300 yards.

  6. #6
    and the hat of int overfl Salem's Avatar
    Join Date
    Aug 2001
    Location
    The edge of the known universe
    Posts
    39,659
    > Here's an example, some classes are instantiated from dat files.
    Are these files loaded as the result of users selecting a file?
    If so, then you should return to the point where the user made the choice.
    If the file was specified on the command line, then exit with an error message.
    If the file was specified using a "File load", then popup an error message and wait for the next user request.
    If you dance barefoot on the broken glass of undefined behaviour, you've got to expect the occasional cut.
    If at first you don't succeed, try writing your phone number on the exam paper.

  7. #7
    Code Goddess Prelude's Avatar
    Join Date
    Sep 2001
    Posts
    9,897
    >Should I throw an exception from within the class even considering that the only way to handle is to abort?
    Yes. Always let the main program decide how to handle an error. Even if you "know for a fact" that the only solution is to abort, you could still be wrong. Also, keeping termination capabilities at roughly the same tier aids in transparency. Finally, you should refrain from printing error messages within your class as well. Both reporting and reacting to exceptions is the job of the controlling program, not the classes that throw the exceptions.

    >And how do I abort gracefully from within a class definition making sure all my resources are cleaned?
    You don't. Clean up the resources that the class uses and pass on the exception to the highest level possible. You never know, a level higher up may be able to successfully recover.
    My best code is written with the delete key.

  8. #8
    (?<!re)tired Mario F.'s Avatar
    Join Date
    May 2006
    Location
    Ireland
    Posts
    8,446
    Ok. Most excellent. Thanks everyone. All have been very helpful. I think I'm getting there...

    In short, I should change my member function declaration to:

    virtual bool load(const std::string&) throw(objects_file_error);

    I should avoid outputting an error message considering I may want to reuse this header in a project/place where there may be in fact some recoverable/reporting procedure the user of this class wants to implement.

    Here's one more question:

    - I'm using assert liberaly to report, mainly, preconditions at function level. On all cases, derived from programming errors. However, even that line seems tricky sometimes. Is the programmer doing an error because he was distracted, or because he doesn't know any better? In other words because my class doesn't inform him of those preconditions. (or worse, because it doesn't enforce them when it should). The example is this:

    CAttribs is a class holding a single data member. An unsigned long representing a packed value of the fighter attributes. Str, Dex, Con, Int, Smartness and Sanity. Each attribute can only be in the range [1,20] (5 bits). 6 attributes x 5 bits = 30 bits.

    The most shocking example is not within the class itself but on an helper function I declared inside the same namespace as that of the class.
    Code:
    // generates random attributes within a range.
    // Returns the packed value to be used in CAttribs(unsigned long) constructor
    unsigned long attributes::RandomAttrib(unsigned int lo, unsigned int hi);
    Inside the funcion definition I assert lo and hi to be >0 and <21, respectively. I also assert lo < hi. The last assert is a no brainer. Good on me. But what about checking for the range?

    Am I doing the right thing leaving it all to an assert? Or should my class enforce this rule?
    Originally Posted by brewbuck:
    Reimplementing a large system in another language to get a 25% performance boost is nonsense. It would be cheaper to just get a computer which is 25% faster.

  9. #9
    Code Goddess Prelude's Avatar
    Join Date
    Sep 2001
    Posts
    9,897
    >virtual bool load(const std::string&) throw(objects_file_error);
    Don't waste your time with exception specifications. They're more trouble than they're worth.

    >Am I doing the right thing leaving it all to an assert? Or should my class enforce this rule?
    I think an assert is fine. Presumably you're documenting what the function expects, so failure to meet those expectations is a breach of contract on the part of the calling code. Your class could enforce it, but then you bring up questions of how to enforce it.
    My best code is written with the delete key.

  10. #10
    (?<!re)tired Mario F.'s Avatar
    Join Date
    May 2006
    Location
    Ireland
    Posts
    8,446
    I thought of making it generic. A template with two nontype arguments representing the range. However, this would bring problems if anything higher than 31 was passed as an argument for the high range. At that point 6 bits would be needed for each attribute. Voil&#225;! Exception handling.

    Another possibility was just one nontype argument and two specializations of 32 and 64 mapping to unsigned long and double, respectively. This would already provide an interesting range of possible values. But... then I would be solving nothing. Range checking would still have to be done probably by asserts just like now.

    Not that I have any interest in making this kind of object generic. Just what occured to me when creating it.

    Anyways, back to earth, thanks Prelude. Assert it is.
    Originally Posted by brewbuck:
    Reimplementing a large system in another language to get a 25% performance boost is nonsense. It would be cheaper to just get a computer which is 25% faster.

  11. #11
    Registered User
    Join Date
    Jan 2005
    Posts
    7,366
    >> virtual bool load(const std::string&) throw(objects_file_error);
    virtual bool load(const std::string&); // throw(objects_file_error)

    >> I thought of making it generic. A template with two nontype arguments representing the range.
    I also think an assert is fine. However, if you used templates you could create a compile time error if the values are out of range. A compile time error is the ultimate indication of programmer error, so if it makes sense in the design it is not a bad idea.

    To cause the compile time error, you have to do some slightly fancy stuff with the template parameters. Alexandrescu's Modern C++ has good examples, although I'm sure there are some online as well.

  12. #12
    Cat without Hat CornedBee's Avatar
    Join Date
    Apr 2003
    Posts
    8,895
    That's what BOOST_STATIC_ASSERT is for.
    So the end result could look like this:
    Code:
    template <unsigned int LOW_BOUND, unsigned int HI_BOUND>
    unsigned long attributes::RandomAttrib(unsigned int lo, unsigned int hi)
    {
      BOOST_STATIC_ASSERT(LOW_BOUND < HI_BOUND);
      BOOST_STATIC_ASSERT(HI_BOUND < (1 << BITS_PER_ATTRIBUTE));
      assert(lo >= LOW_BOUND);
      assert(lo < hi);
      assert(hi <= HI_BOUND);
    }
    All in all, I think it's a waste of time. Document that the values lo and hi must be between 0 and (1 << BITS_PER_ATTRIBUTE) and assert against those only. The template arguments add nothing.
    All the buzzt!
    CornedBee

    "There is not now, nor has there ever been, nor will there ever be, any programming language in which it is the least bit difficult to write bad code."
    - Flon's Law

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. global namespace errors
    By stubaan in forum C++ Programming
    Replies: 9
    Last Post: 04-02-2008, 03:11 PM
  2. Ten Errors
    By AverageSoftware in forum Contests Board
    Replies: 0
    Last Post: 07-20-2007, 10:50 AM
  3. Header File Errors...
    By Junior89 in forum C++ Programming
    Replies: 5
    Last Post: 07-08-2007, 12:28 AM
  4. executing errors
    By s0ul2squeeze in forum C++ Programming
    Replies: 3
    Last Post: 03-26-2002, 01:43 PM