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.
2 Answers 2
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.
enum
s 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 Error
s 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!
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.
-
\$\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– Ethan2017年03月11日 00:47:43 +00:00Commented Mar 11, 2017 at 0:47
-
\$\begingroup\$ If that's the case, why have save the value of the passed
Er_Type
? You could just have abool
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 nameChangeMessage
because it is misleading. \$\endgroup\$Ben Steffan– Ben Steffan2017年03月11日 15:10:47 +00:00Commented Mar 11, 2017 at 15:10 -
\$\begingroup\$ Why have a bool flag when it is unnecessary? \$\endgroup\$Ethan– Ethan2017年03月11日 18:08:59 +00:00Commented Mar 11, 2017 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 flagbool error_set
that represents whether you have an error or not. \$\endgroup\$Ben Steffan– Ben Steffan2017年03月11日 19:39:08 +00:00Commented Mar 11, 2017 at 19:39 -
\$\begingroup\$ I see what your saying now. Good idea \$\endgroup\$Ethan– Ethan2017年03月11日 22:42:10 +00:00Commented Mar 11, 2017 at 22:42