The primary virtue of this approach is that it is extremely simple. However, using return codes has a number of drawbacks which can quickly become apparent when used in non-trivial cases:
First, return values can be cryptic -- if a function returns -1, is it trying to indicate an error, or is that actually a valid return value? It’s often hard to tell without digging into the guts of the function or consulting documentation.
Second, functions can only return one value, so what happens when you need to return both a function result and a possible error code? Consider the following function:
double divide(int x, int y)
{
return static_cast<double>(x)/y;
}
This function is in desperate need of some error handling, because it will crash if the user passes in 0 for parameter y. However, it also needs to return the result of x/y. How can it do both? The most common answer is that either the result or the error handling will have to be passed back as a reference parameter, which makes for ugly code that is less convenient to use. For example:
include <iostream>
double divide(int x, int y, bool& outSuccess)
{
if (y == 0)
{
outSuccess = false;
return 0.0;
}
outSuccess = true;
return static_cast<double>(x)/y;
}
int main()
{
bool success {}; // we must now pass in a bool value to see if the call was successful
double result { divide(5, 3, success) };
if (!success) // and check it before we use the result
std::cerr << "An error occurred" << std::endl;
else
std::cout << "The answer is " << result << '\n';
}
Third, in sequences of code where many things can go wrong, error codes have to be checked constantly. Consider the following snippet of code that involves parsing a text file for values that are supposed to be there:
std::ifstream setupIni { "setup.ini" }; // open setup.ini for reading
// If the file couldn't be opened (e.g. because it was missing) return some error enum
if (!setupIni)
return ERROR_OPENING_FILE;
// Now read a bunch of values from a file
if (!readIntegerFromFile(setupIni, m_firstParameter)) // try to read an integer from the file
return ERROR_READING_VALUE; // Return enum value indicating value couldn't be read
if (!readDoubleFromFile(setupIni, m_secondParameter)) // try to read a double from the file
return ERROR_READING_VALUE;
if (!readFloatFromFile(setupIni, m_thirdParameter)) // try to read a float from the file
return ERROR_READING_VALUE;
We haven’t covered file access yet, so don’t worry if you don’t understand how the above works -- just note the fact that every call requires an error-check and return back to the caller. Now imagine if there were twenty parameters of differing types -- you’re essentially checking for an error and returning ERROR_READING_VALUE twenty times! All of this error checking and returning values makes determining what the function is trying to do much harder to discern.
Fourth, return codes do not mix with constructors very well. What happens if you’re creating an object and something inside the constructor goes catastrophically wrong? Constructors have no return type to pass back a status indicator, and passing one back via a reference parameter is messy and must be explicitly checked. Furthermore, even if you do this, the object will still be created and then has to be dealt with or disposed of.
Finally, when an error code is returned to the caller, the caller may not always be equipped to handle the error. If the caller doesn’t want to handle the error, it either has to ignore it (in which case it will be lost forever), or return the error up the stack to the function that called it. This can be messy and lead to many of the same issues noted above.