6.3. Return Types and the return
Statement
A return
statement terminates the function that is currently executing and returns control to the point from which the function was called. There are two forms of return
statements:
return;
return expression;
6.3.1. Functions with No Return Value
FundamentalA return
with no value may be used only in a function that has a return type of void
. Functions that return void
are not required to contain a return
. In a void
function, an implicit return
takes place after the function’s last statement.
Typically, void
functions use a return
to exit the function at an intermediate point. This use of return
is analogous to the use of a break
statement (§ 5.5.1, p. 190) to exit a loop. For example, we can write a swap
function that does no work if the values are identical:
void swap(int &v1, int &v2)
{
// if the values are already the same, no need to swap, just return
if (v1 == v2)
return;
// if we're here, there's work to do
int tmp = v2;
v2 = v1;
v1 = tmp;
// no explicit return necessary
}
This function first checks if the values are equal and, if so, exits the function. If the values are unequal, the function swaps them. An implicit return occurs after the last assignment statement.
A function with a void
return type may use the second form of the return
statement only to return the result of calling another function that returns void
. Returning any other expression from a void
function is a compile-time error.
6.3.2. Functions That Return a Value
FundamentalThe second form of the return
statement provides the function’s result. Every return in a function with a return type other than void
must return a value. The value returned must have the same type as the function return type, or it must have a type that can be implicitly converted (§ 4.11, p. 159) to that type.
Although C++ cannot guarantee the correctness of a result, it can guarantee that every return
includes a result of the appropriate type. Although it cannot do so in all cases, the compiler attempts to ensure that functions that return a value are exited only through a valid return
statement. For example:
// incorrect return values, this code will not compile
bool str_subrange(const string &str1, const string &str2)
{
// same sizes: return normal equality test
if (str1.size() == str2.size())
return str1 == str2; // ok: == returns bool
// find the size of the smaller string; conditional operator, see § 4.7 (p. 151)
auto size = (str1.size() < str2.size())
? str1.size() : str2.size();
// look at each element up to the size of the smaller string
for (decltype(size) i = 0; i != size; ++i) {
if (str1[i] != str2[i])
return; // error #1: no return value; compiler should detect this error
}
// error #2: control might flow off the end of the function without a return
// the compiler might not detect this error
}
The return
from within the for
loop is an error because it fails to return a value. The compiler should detect this error.
The second error occurs because the function fails to provide a return
after the loop. If we call this function with one string
that is a subset of the other, execution would fall out of the for
. There should be a return to handle this case. The compiler may or may not detect this error. If it does not detect the error, what happens at run time is undefined.
WARNING
Failing to provide a return
after a loop that contains a return
is an error. However, many compilers will not detect such errors.
How Values Are Returned
Values are returned in exactly the same way as variables and parameters are initialized: The return value is used to initialize a temporary at the call site, and that temporary is the result of the function call.
It is important to keep in mind the initialization rules in functions that return local variables. As an example, we might write a function that, given a counter, a word, and an ending, gives us back the plural version of the word if the counter is greater than 1:
// return the plural version of word if ctr is greater than 1
string make_plural(size_t ctr, const string &word,
const string &ending)
{
return (ctr > 1) ? word + ending : word;
}
The return type of this function is string
, which means the return value is copied to the call site. This function returns a copy of word
, or it returns an unnamed temporary string
that results from adding word
and ending
.
As with any other reference, when a function returns a reference, that reference is just another name for the object to which it refers. As an example, consider a function that returns a reference to the shorter of its two string
parameters:
// return a reference to the shorter of two strings
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
The parameters and return type are references to const string
. The string
s are not copied when the function is called or when the result is returned.
Never Return a Reference or Pointer to a Local Object
When a function completes, its storage is freed (§ 6.1.1, p. 204). After a function terminates, references to local objects refer to memory that is no longer valid:
// disaster: this function returns a reference to a local object
const string &manip()
{
string ret;
// transform ret in some way
if (!ret.empty())
return ret; // WRONG: returning a reference to a local object!
else
return "Empty"; // WRONG: "Empty" is a local temporary string
}
Both of these return
statements return an undefined value—what happens if we try to use the value returned from manip
is undefined. In the first return
, it should be obvious that the function returns a reference to a local object. In the second case, the string literal is converted to a local temporary string
object. That object, like the string
named s
, is local to manip
. The storage in which the temporary resides is freed when the function ends. Both return
s refer to memory that is no longer available.
TIP
One good way to ensure that the return is safe is to ask: To what preexisting object is the reference referring?
For the same reasons that it is wrong to return a reference to a local object, it is also wrong to return a pointer to a local object. Once the function completes, the local objects are freed. The pointer would point to a nonexistent object.
Functions That Return Class Types and the Call Operator
Like any operator the call operator has associativity and precedence (§ 4.1.2, p. 136). The call operator has the same precedence as the dot and arrow operators (§ 4.6, p. 150). Like those operators, the call operator is left associative. As a result, if a function returns a pointer, reference or object of class type, we can use the result of a call to call a member of the resulting object.
For example, we can determine the size of the shorter string
as follows:
// call the size member of the string returned by shorterString
auto sz = shorterString(s1, s2).size();
Because these operators are left associative, the result of shorterString
is the left-hand operand of the dot operator. That operator fetches the size
member of that string
. That member is the left-hand operand of the second call operator.
Reference Returns Are Lvalues
Whether a function call is an lvalue (§ 4.1.1, p. 135) depends on the return type of the function. Calls to functions that return references are lvalues; other return types yield rvalues. A call to a function that returns a reference can be used in the same ways as any other lvalue. In particular, we can assign to the result of a function that returns a reference to nonconst
:
char &get_val(string &str, string::size_type ix)
{
return str[ix]; // get_val assumes the given index is valid
}
int main()
{
string s("a value");
cout << s << endl; // prints a value
get_val(s, 0) = 'A'; // changes s[0] to A
cout << s << endl; // prints A value
return 0;
}
It may be surprising to see a function call on the left-hand side of an assignment. However, nothing special is involved. The return value is a reference, so the call is an lvalue. Like any other lvalue, it may appear as the left-hand operand of the assignment operator.
If the return type is a reference to const
, then (as usual) we may not assign to the result of the call:
shorterString("hi", "bye") = "X"; // error: return value is const
List Initializing the Return Value
C++11Under the new standard, functions can return a braced list of values. As in any other return, the list is used to initialize the temporary that represents the function’s return. If the list is empty, that temporary is value initialized (§ 3.3.1, p. 98). Otherwise, the value of the return depends on the function’s return type.
As an example, recall the error_msg
function from § 6.2.6 (p. 220). That function took a varying number of string
arguments and printed an error message composed from the given string
s. Rather than calling error_msg
, in this function we’ll return a vector
that holds the error-message string
s:
vector<string> process()
{
// . . .
// expected and actual are strings
if (expected.empty())
return {}; // return an empty vector
else if (expected == actual)
return {"functionX", "okay"}; // return list-initialized vector
else
return {"functionX", expected, actual};
}
In the first return statement, we return an empty list. In this case, the vector
that process
returns will be empty. Otherwise, we return a vector
initialized with two or three elements depending on whether expected
and actual
are equal.
In a function that returns a built-in type, a braced list may contain at most one value, and that value must not require a narrowing conversion (§ 2.2.1, p. 43). If the function returns a class type, then the class itself defines how the intiailizers are used (§ 3.3.1, p. 99).
Return from main
There is one exception to the rule that a function with a return type other than void
must return a value: The main
function is allowed to terminate without a return. If control reaches the end of main
and there is no return, then the compiler implicitly inserts a return of 0.
As we saw in § 1.1 (p. 2), the value returned from main
is treated as a status indicator. A zero return indicates success; most other values indicate failure. A nonzero value has a machine-dependent meaning. To make return values machine independent, the cstdlib
header defines two preprocessor variables (§ 2.3.2, p. 54) that we can use to indicate success or failure:
int main()
{
if (some_failure)
return EXIT_FAILURE; // defined in cstdlib
else
return EXIT_SUCCESS; // defined in cstdlib
}
Because these are preprocessor variables, we must not precede them with std::
, nor may we mention them in using
declarations.
Recursion
A function that calls itself, either directly or indirectly, is a recursive function. As an example, we can rewrite our factorial function to use recursion:
// calculate val!, which is 1 * 2 * 3 . . . * val
int factorial(int val)
{
if (val > 1)
return factorial(val-1) * val;
return 1;
}
In this implementation, we recursively call factorial
to compute the factorial of the numbers counting down from the original value in val
. Once we have reduced val
to 1
, we stop the recursion by returning 1
.
There must always be a path through a recursive function that does not involve a recursive call; otherwise, the function will recurse “forever,” meaning that the function will continue to call itself until the program stack is exhausted. Such functions are sometimes described as containing a recursion loop. In the case of factorial
, the stopping condition occurs when val
is 1
.
The following table traces the execution of factorial
when passed the value 5.
Trace of factorial(5)
Call | Returns | Value |
---|---|---|
factorial(5) | factorial(4) * 5 | 120 |
factorial(4) | factorial(3) * 4 | 24 |
factorial(3) | factorial(2) * 3 | 6 |
factorial(2) | factorial(1) * 2 | 2 |
factorial(1) | 1 | 1 |
INFO
The main
function may not call itself.
INFO
Exercises Section 6.3.2
Exercise 6.30: Compile the version of str_subrange
as presented on page 223 to see what your compiler does with the indicated errors.
Exercise 6.31: When is it valid to return a reference? A reference to const
?
Exercise 6.32: Indicate whether the following function is legal. If so, explain what it does; if not, correct any errors and then explain it.
int &get(int *arry, int index) { return arry[index]; }
int main() {
int ia[10];
for (int i = 0; i != 10; ++i)
get(ia, i) = i;
}
Exercise 6.33: Write a recursive function to print the contents of a vector
.
Exercise 6.34: What would happen if the stopping condition in factorial
were
if (val != 0)
Exercise 6.35: In the call to fact
, why did we pass val - 1
rather than val--?
6.3.3. Returning a Pointer to an Array
Because we cannot copy an array, a function cannot return an array. However, a function can return a pointer or a reference to an array (§ 3.5.1, p. 114). Unfortunately, the syntax used to define functions that return pointers or references to arrays can be intimidating. Fortunately, there are ways to simplify such declarations. The most straightforward way is to use a type alias (§ 2.5.1, p. 67):
typedef int arrT[10]; // arrT is a synonym for the type array of ten ints
using arrtT = int[10]; // equivalent declaration of arrT; see § 2.5.1 (p. 68)
arrT* func(int i); // func returns a pointer to an array of five ints
Here arrT
is a synonym for an array of ten int
s. Because we cannot return an array, we define the return type as a pointer to this type. Thus, func
is a function that takes a single int
argument and returns a pointer to an array of ten int
s.
Declaring a Function That Returns a Pointer to an Array
To declare func
without using a type alias, we must remember that the dimension of an array follows the name being defined:
int arr[10]; // arr is an array of ten ints
int *p1[10]; // p1 is an array of ten pointers
int (*p2)[10] = &arr; // p2 points to an array of ten ints
As with these declarations, if we want to define a function that returns a pointer to an array, the dimension must follow the function’s name. However, a function includes a parameter list, which also follows the name. The parameter list precedes the dimension. Hence, the form of a function that returns a pointer to an array is:
Type (*function(parameter_list))[dimension]
As in any other array declaration, Type is the type of the elements and dimension is the size of the array. The parentheses around (*
function(
parameter_list))
are necessary for the same reason that they were required when we defined p2
. Without them, we would be defining a function that returns an array of pointers.
As a concrete example, the following declares func
without using a type alias:
int (*func(int i))[10];
To understand this declaration, it can be helpful to think about it as follows:
func(int)
says that we can callfunc
with anint
argument.(*func(int))
says we can dereference the result of that call.(*func(int))[10]
says that dereferencing the result of a call tofunc
yields an array of size ten.int (*func(int))[10]
says the element type in that array isint
.
Using a Trailing Return Type
C++11Under the new standard, another way to simplify the declaration of func
is by using a trailing return type. Trailing returns can be defined for any function, but are most useful for functions with complicated return types, such as pointers (or references) to arrays. A trailing return type follows the parameter list and is preceded by ->
. To signal that the return follows the parameter list, we use auto
where the return type ordinarily appears:
// fcn takes an int argument and returns a pointer to an array of ten ints
auto func(int i) -> int(*)[10];
Because the return type comes after the parameter list, it is easier to see that func
returns a pointer and that that pointer points to an array of ten int
s.
Using decltype
As another alternative, if we know the array(s) to which our function can return a pointer, we can use decltype
to declare the return type. For example, the following function returns a pointer to one of two arrays, depending on the value of its parameter:
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
// returns a pointer to an array of five int elements
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even; // returns a pointer to the array
}
The return type for arrPtr
uses decltype
to say that the function returns a pointer to whatever type odd
has. That object is an array, so arrPtr
returns a pointer to an array of five int
s. The only tricky part is that we must remember that decltype
does not automatically convert an array to its corresponding pointer type. The type returned by decltype
is an array type, to which we must add a *
to indicate that arrPtr
returns a pointer.
INFO
Exercises Section 6.3.3
Exercise 6.36: Write the declaration for a function that returns a reference to an array of ten string
s, without using either a trailing return, decltype
, or a type alias.
Exercise 6.37: Write three additional declarations for the function in the previous exercise. One should use a type alias, one should use a trailing return, and the third should use decltype
. Which form do you prefer and why?
Exercise 6.38: Revise the arrPtr
function on to return a reference to the array.