C++ Programming
Returning values
editWhen declaring a function, you must declare it in terms of the type that it will return, this is done in three steps, in the function declaration, the function implementation (if distinct) and on the body of the same function with the return
keyword.
- Functions with results
You might have noticed by now that some of the functions yield results. Other functions perform an action but don't return
a value.
Other ways to get a value from a function is to use a pointer or a reference as argument or use a global variable
- Get more that a single value from a function
The return type determines the capacity, any type will work from an array or a std::vector, a struct or a class, it is only restricted by the return type you chose.
- That raises some questions
- What happens if you call a function and you don't do anything with the result (i.e. you don't assign it to a variable or use it as part of a larger expression)?
- What happens if you use a function without a result as part of an expression, like newLine() + 7?
- Can we write functions that yield results, or are we stuck with things like newLine and printTwice?
The answer to the third question is "yes, you can write functions that returns values,". For now I will leave it up to you to answer the other two questions by trying them out. Any time you have a question about what is legal or illegal in C++, a first step to find out is to ask the compiler. However you should be aware of two issues, that we already mentioned when introducing the compiler: First a compiler may have bugs just like any other software, so it happens that not every source code which is forbidden in C++ is properly rejected by the compiler, and vice versa. The other issue is even more dangerous: You can write programs in C++ which a C++ implementation is not required to reject, but whose behavior is not defined by the language. Needless to say, running such a program can, and occasionally will, do harmful things to the system it is running or produce corrupt output!
For example:
int MyFunc(); // returns an int
SOMETYPE MyFunc(); // returns a SOMETYPE
int* MyFunc(); // returns a pointer to an int
SOMETYPE *MyFunc(); // returns a pointer to a SOMETYPE
SOMETYPE &MyFunc(); // returns a reference to a SOMETYPE
If you have understood the syntax of pointer declarations, the declaration of a function that returns a pointer or a reference should seem logical. The above piece of code shows how to declare a function that will return a reference or a pointer; below are outlines of what the definitions (implementations) of such functions would look like:
SOMETYPE *MyFunc(int *p)
{
//...
return p;
}
SOMETYPE &MyFunc(int &r)
{
//...
return r;
}
The return
statement causes execution to jump from the current function to whatever function called the current function. An optional a result (return variable) can be returned. A function may have more than one return
statement (but returning the same type).
- Syntax
return;
return value;
Within the body of the function, the return
statement should NOT return
a pointer or a reference that has the address in memory of a local variable that was declared within the function, because as soon as the function exits, all local variables are destroyed and your pointer or reference will be pointing to some place in memory which you no longer own, so you cannot guarantee its contents. If the object to which a pointer refers is destroyed, the pointer is said to be a dangling pointer until it is given a new value; any use of the value of such a pointer is invalid. Having a dangling pointer like that is dangerous; pointers or references to local variables must not be allowed to escape the function in which those local (aka automatic) variables live.
However, within the body of your function, if your pointer or reference has the address in memory of a data type, struct, or class that you dynamically allocated the memory for, using the new operator, then returning said pointer or reference would be reasonable:
SOMETYPE *MyFunc() //returning a pointer that has a dynamically
{ //allocated memory address is valid code
int *p = new int[5];
//...
return p;
}
In most cases, a better approach in that case would be to return
an object such as a smart pointer which could manage the memory; explicit memory management using widely distributed calls to new
and delete
(or malloc
and free
) is tedious, verbose and error prone. At the very least, functions which return
dynamically allocated resources should be carefully documented. See this book's section on memory management for more details.
const SOMETYPE *MyFunc(int *p)
{
//...
return p;
}
In this case the SOMETYPE object pointed to by the returned pointer may not be modified, and if SOMETYPE is a class then only const member functions may be called on the SOMETYPE object.
If such a const return
value is a pointer or a reference to a class then we cannot call non-const methods on that pointer or reference since that would break our agreement not to change it.
Static returns
editWhen a function returns a variable (or a pointer to one) that is statically located, one must keep in mind that it will be possible to overwrite its content each time a function that uses it is called. If you want to save the return value of this function, you should manually save it elsewhere. Most such static returns use global variables.
Of course, when you save it elsewhere, you should make sure to actually copy the value(s) of this variable to another location. If the return value is a struct, you should make a new struct, then copy over the members of the struct.
One example of such a function is the Standard C Library function localtime.
Return "codes" (best practices)
editThere are 2 kinds of behaviors :
Positive means success
editThis is the "logical" way to think, and as such the one used by almost all beginners. In C++, this takes the form of a boolean true/false test, where "true" (also 1 or any non-zero number) means success, and "false" (also 0) means failure.
The major problem of this construct is that all errors return the same value (false), so you must have some kind of externally visible error code in order to determine where the error occurred. For example:
bool bOK;
if (my_function1())
{
// block of instruction 1
if (my_function2())
{
// block of instruction 2
if (my_function3())
{
// block of instruction 3
// Everything worked
error_code = NO_ERROR;
bOK = true;
}
else
{
//error handler for function 3 errors
error_code = FUNCTION_3_FAILED;
bOK = false;
}
}
else
{
//error handler for function 2 errors
error_code = FUNCTION_2_FAILED;
bOK = false;
}
}
else
{
//error handler for function 1 errors
error_code = FUNCTION_1_FAILED;
bOK = false;
}
return bOK;
As you can see, the else blocks (usually error handling) of my_function1 can be really far from the test itself; this is the first problem. When your function begins to grow, it's often difficult to see the test and the error handling at the same time.
This problem can be compensated by source code editor features such as folding, or by testing for a function returning "false" instead of true.
if (!my_function1()) // or if (my_function1() == false)
{
//error handler for function 1 errors
//...
This can also make the code look more like the "0 means success" paradigm, but a little less readable.
The second problem of this construct is that it tends to break up logical tests (my_function2 is one level more indented, my_function3 is 2 levels indented) which causes legibility problems.
One advantage here is that you follow the structured programming principle of a function having a single entry and a single exit.
The Microsoft Foundation Class Library (MFC) is an example of a standard library that uses this paradigm.
0 means success
editThis means that if a function returns 0, the function has completed successfully. Any other value means that an error occurred, and the value returned may be an indication of what error occurred.
The advantage of this paradigm is that the error handling is closer to the test itself. For example the previous code becomes:
if (0 != my_function1())
{
//error handler for function 1 errors
return FUNCTION_1_FAILED;
}
// block of instruction 1
if (0 != my_function2())
{
//error handler for function 2 errors
return FUNCTION_2_FAILED;
}
// block of instruction 2
if (0 != my_function3())
{
//error handler for function 3 errors
return FUNCTION_3_FAILED;
}
// block of instruction 3
// Everything worked
return 0; // NO_ERROR
In this example, this code is more readable (this will not always be the case). However, this function now has multiple exit points, violating a principle of structured programming.
The C Standard Library (libc) is an example of a standard library that uses this paradigm.