5
\$\begingroup\$

I have been doing some searching into how to properly handle errors in C++. I have found that the two most common techniques are exceptions and enumerations. I have created a simple class to expand on the enumeration method. It's main features are boolean operator overloading to return if the object actually has an error "attached to it", and the ability to set messages at the point where the error occured. It uses an enumeration so that default error messages can be set. The ones in my code are for my current program, and are meant to be examples. Here is my code:

enum Err_Type {
    NO_ERR, EMPTY, INVALID_INPUT, OUT_OF_RANGE,
    INVALID_FUNC_NAME, INVALID_PARANS, INVALID_OPERATOR, DIV_ZERO, INVALID_BASE,
    FILE_IO_ERROR, CUSTOM
};                                                                                      

class Error                                                                             
{                                                                                       
public:                                                                                 
    // Constructors                                                                     
    Error() { Type = NO_ERR; Message = ""; }                                            
    Error(Err_Type type) { Type = type; SetMessage(type); }                             
    Error(std::string msg) { Type = CUSTOM; Message = msg; }                            

    // Operator Overloading                                                             
    explicit operator bool() const { return Type != NO_ERR; }                           
    bool operator !() const { return Type == NO_ERR; }                                  
    void operator ()(Err_Type type) { Type = type; SetMessage(type); }                  

    // Public Methods                                                                   
    void ChangeMessage(std::string msg) { Type = CUSTOM; Message = msg; }               
    std::string GetErrMessage() { return Message; }                                     
    void DisplayMessage() { std::cout << Message << endl; }                             


private:                                                                                

    Err_Type Type;                                                                      
    std::string Message;                                                                
    void SetMessage(Err_Type type)                                                      
    {                                                                                   
    switch (type)                                                                       
    {                                                                                   
    case EMPTY: Message = "Entry cannot be empty!"; break;                              
    case INVALID_INPUT: Message = "Entry contained invalid characters!"; break;         
    case OUT_OF_RANGE: Message = "Entry contains a value that is out of range!"; break; 
    case INVALID_FUNC_NAME: Message = "Entry contained invalid function name!"; break;  
    case INVALID_PARANS: Message = "Entry contained a parantheses error!"; break;       
    case INVALID_OPERATOR: Message = "Entry contained an operator error!"; break;       
    case DIV_ZERO: Message = "Entry contained an attempt to divide by zero!"; break;    
    case INVALID_BASE: Message = "Entry contained an invalid base!"; break;             
    case FILE_IO_ERROR: Message = "File opening error!"; break;                         
    case NO_ERR: Message = "";                                                          
    }                                                                                   
    }                                                                                   
};  

I know that it is very simple and basic, and maybe not very good (I am a novice and a student to C++), so I would like some suggestions for improvements. My main objective with this is to allow my functions to pass around error information easily, and also to construct the error messages at the point where the errors are found. All suggestions are appreciated.

\$\endgroup\$
3
\$\begingroup\$

I have found that the two most common techniques are exceptions and enumerations

That works for small and short lived projects but quickly becomes a problem when the types of errors you have deal with keep increasing.

enums are best when the enumerators are fixed for most part. For example, you can create a simple enum like below:

enum Status {SUCCESS, FAILURE};

The reasons for failure is ever expanding in real world projects. It's best to capture the different types of errors through simple class hierarchies. Example:

struct Error
{
   virtual ~Error() {}
   virtual std::string getMessage() = 0;
};

Now, you can use a class that captures results of an operation or a function call. It depends on enum Status and Error as its member variables. Of course, you can add as many convenience functions as you see fit to it.

struct Result
{
   Result() : s(SUCCESS), e(nullptr) {}

   Result(Error* in) : s(in == nullptr ? SUCCESS : FAILURE), e(in) {}

   operator bool () const
   {
      return (s == SUCCESS);
   }

   bool operator!() const
   {
      return (s != SUCCESS);
   }

   std::string getMessage() const
   {
      if ( nullptr == e )
      {
         return "";
      }
      else
      {
         return e->getMessage();
      }
   }

   Status s;
   std::shared_ptr<Error> e;
};

You can add different types of Errors by sub-classing Error. For example:

struct Empty : Error
{
   std::string getMessage()
   {
      return "Entry cannot be empty!";
   }
};

Now you have a framework where the basic objects are in place. The only things that will keep on increasing are the types of errors your application deals with. That's easy to do by sub-classing Error. Here's a small program that shows how they can be used:

#include <iostream>
#include <string>
#include <memory>

// Using a namespace helps with avoiding conflicts
namespace MyApp
{
   enum Status {SUCCESS, FAILURE};

   struct Error
   {
      virtual ~Error() {}
      virtual std::string getMessage() = 0;
   };

   struct Result
   {
      Result() : s(SUCCESS), e(nullptr) {}

      Result(Error* in) : s(in == nullptr ? SUCCESS : FAILURE), e(in) {}

      operator bool () const
      {
         return (s == SUCCESS);
      }

      bool operator!() const
      {
         return (s != SUCCESS);
      }

      std::string getMessage() const
      {
         if ( nullptr == e )
         {
            return "";
         }
         else
         {
            return e->getMessage();
         }
      }

      Status s;
      std::shared_ptr<Error> e;
   };

   std::ostream& operator<<(std::ostream& out, Result const& r)
   {
      return (out << ( r ? "SUCCESS" : "FAILURE") << " " << r.getMessage());
   }

   struct Empty : Error
   {
      std::string getMessage()
      {
         return "Entry cannot be empty!";
      }
   };

   struct InvalidInput : Error 
   {
      std::string getMessage()
      {
         return "Entry contained invalid characters!";
      }
   };

   struct OutOfRange : Error
   {
      std::string getMessage()
      {
         return "Entry contains a value that is out of range!";
      }
   };

   // Add other subtypes of Error corresponding to

   // INVALID_FUNC_NAME
   // INVALID_PARENS
   // INVALID_OPERATOR
   // DIV_ZERO
   // INVALID_BASE
   // FILE_IO_ERROR
}    

int main()
{
   using namespace MyApp;

   Result r1;
   Result r2(new OutOfRange);

   std::cout << r1 << std::endl;
   std::cout << r2 << std::endl;
}

Output of the program:

SUCCESS 
FAILURE Entry contains a value that is out of range!
\$\endgroup\$
4
\$\begingroup\$

Orthography

I guess it is a typo, but INVALID_PARANS should probably be called INVALID_PARAMS.

The message for INVALID_PARANS also contains the word "parantheses", which ought to be "parentheses".

Style

Usually, it is preferred to have every enumerator on it's own line. This also applies to methods, meaning that you should have every statement on it's own line, e.g. your ChangeMessage method should become something like

void ChangeMessage(std::string msg) {
    Type = CUSTOM;
    Message = msg;
}

Also, you should start indenting your methods. Everytime you enter a new block (= write a new {) you should go one level of indentation deeper, i. e. add a tab or 2, 4, or 8 spaces (according to your personal preference). This means that the SetMessage method should look more like this:

void SetMessage(Err_Type type) {
    switch (type) {
        case EMPTY:
            Message = "Entry cannot be empty!";
            break;
        ...
    }
}

(Also applies to all other methods)

Another important point is your naming. Although there are no fixed rules for this in c++, you'll usually see member variables, methods and class names beginning with a lowercase letter. Also, if you have compound names, most c++ programmers prefer the so called snake_case style, meaning that each part of the name should be written in lowercase and joined with an underscore (this style is used throughout the standard library, but there are other possibilities, such as camelCase, if you are uncomfortable with it). Most important, however, is that you stay consistent. Thus Err_Type should either be called err_type or errType in line with the style you choose.

const-Correctness

Since GetErrMessage and DisplayMessage do not alter any member variables, they should be marked const.

Improvements

Since you tagged this question with c++11, I advise you to use enum class instead of enum to keep your global scope clean, since enum just puts all enumerators into global scope. Also, your implementation currently lacks a method to get the the actual Err_Type that an Error object represents.

GetErrMessage returns a string by value, which means making a copy of it everytime the method is called. Most of the time, returning a const std::string& would be the better choice here.

Since this class handles errors, printing to std::cerr instead of std::cout would be the appropriate behaviour for DisplayMessage. However, this method shouldn't actually exist in the first place, since it violates the SOLID principles. What if somebody wants to output an error into a file? Bad Luck. Also, it is highly discouraged to use std::endl in most cases, since it not only writes a line terminator but also flushes the buffer, which can lead to severe performance loss (assuming that the endl in your code is indeed std::endl).

Furthermore, the method ChangeMessage is a bit misleading: It does not only change the message, but it also silently changes the error type to CUSTOM. You should either change your method to allow custom error messages for all kinds of errors or rename it to reflect what it does.

\$\endgroup\$
  • \$\begingroup\$ Thank you for your thoughts. I don't think you understand how I am using the enumerations. They are for default messages, only. All that is to be cared about is whether or not the Error object is actually "attached" to an error. That is, whether or not the Error object's Err_Type is NO_ERR or not NO_ERR. \$\endgroup\$ – Ethan Mar 11 '17 at 0:47
  • \$\begingroup\$ If that's the case, why have save the value of the passed Er_Type? You could just have a bool flag saying whether there is an error or not, and set the error message when the object is constructed. Still, whether you do this or not, you should still change the name ChangeMessage because it is misleading. \$\endgroup\$ – Ben Steffan Mar 11 '17 at 15:10
  • \$\begingroup\$ Why have a bool flag when it is unnecessary? \$\endgroup\$ – Ethan Mar 11 '17 at 18:08
  • 1
    \$\begingroup\$ To avoid carrying around the Err_Type in your class. Since you never really do anything with it, you could just have a flag bool error_set that represents whether you have an error or not. \$\endgroup\$ – Ben Steffan Mar 11 '17 at 19:39
  • \$\begingroup\$ I see what your saying now. Good idea \$\endgroup\$ – Ethan Mar 11 '17 at 22:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.