Just as C++ has three ways of passing data into functions, it also has three ways for functions to return their results with the return operator. It can return a value, a pointer, or a reference. However, returning a local variable (i.e., a variable defined inside the function) by pointer or reference is problematic. The following sections demonstrate this problem and its solutions.
struct part
{
char type;
int id;
};
Example data: The part structure.
Each example consists of two functions: a supplier and a client. The supplier represents a function completing a useful but otherwise abstract task. It creates and returns a part object, simulating the behavior of an authentic function. The client has a single concrete statement that calls the supplier and assigns the returned object to a variable defined in the client. Although the examples could use fundamental data types, passing and returning an object further demonstrates objects, which are increasingly important in later chapters.
Return-By-Value
part supplier()
{
part a = { 'd', 10 };
return a;
}
void client()
{
part x = supplier();
}
Return-by-value.
Return-by-value works similarly to pass-by-value: the value in the local supplier variable, a, is copied as the function's return value, and is deallocated when the function ends. The assignment operator saves the returned copy in the client variable x.
Returned: data copy
Notes:
The default return mechanism
The function returns a copy of the data value
Any valid data type, including structures and classes, may be returned
Return-By-Pointer
part* supplier()
{
part a = { 'd', 10 };
return &a; // error
}
void client()
{
part* x = supplier();
}
(a)
(b)
Incorrect return-by-pointer. Functions can return data by pointer, but programmers must be careful with what they return. The function is syntactically correct and compiles. Nevertheless, this version has a logical error. Some but not all compilers will issue a non-fatal warning for the return &a; statement.
The function returns the address of a part object, so the return type, part*, is correct. But the local variable a is deallocated when the function ends, so x points to deallocated memory. Once the program deallocates memory, it becomes available for reallocation, potentially overwriting previously saved data.
The arrow represents a pointer: x points to a deallocated part object (represented by the dashed-line box).
part* supplier()
{
part* a = new part { 'd', 10 };
return a;
}
void client()
{
part* x = supplier();
}
(a)
(b)
part* supplier()
{
static part a = { 'd', 10 };
return &a;
}
void client()
{
part* x = supplier();
}
(c)
(d)
Correct return-by-pointer. Both example programs are syntactically and logically correct.
Memory allocated on the heap with the new operator is not deallocated when the function ends. The local variable a goes out of scope when the function returns, but the client saves the address in x. Programmers must remember to delete allocated heap memory when the program no longer needs it, otherwise it becomes a memory leak.
The small boxes represent the local variables a and x, the large box represents the new part object, and the solid arrows represent the pointers to the object. The supplier returns the address saved in a to the client, which saves it in x - indicated by the dotted arrow. The dashed-line box indicates that the program deallocates a when the function ends, but the object remains allocated and usable.
This example adds the static keyword to the code presented in Figure 2, creating a solution without a logical error. C++ does not allocate static variables on the stack or the heap; it allocates them when the operating system loads the program into memory, and they remain allocated until the program ends. The local variable a goes out of scope when the function ends, but its memory is not deallocated, and it retains the saved data. supplier returns the object's address to client, which saves it in the pointer x. Although a is not in scope, it remains in memory, accessible indirectly through x.
Compare to Figure 2(b). x points to a, which the solid box suggests remains allocated.
Returned: the address of data
Notes:
Return-by-pointer is a special case of return-by-value - the value returned is an address
The returned address must point to static or dynamic (allocated with new) data
Return-By-Reference
part& supplier()
{
part a = { 'd', 10 };
return a; // error
}
void client()
{
part& x = supplier();
}
(a)
(b)
Incorrect return-by-reference. Functions can also return data by reference, but require programmers to exercise caution again. This version is syntactically correct, compiling without error, but having a logical error. Also, like return-by-pointer, some but not all compilers will issue a non-fatal warning at the return a; statement.
The compiler makes references by mapping two variable names, each in a different scope, to the same memory address. In this example, the supplier defines object a and allocates its memory. The compiler maps the variable x in the client to the memory of variable a in the supplier. But a is deallocated when the supplier ends, leaving x to refer to deallocated memory.
The supplier defines the local variable a, which the program deallocates when the function ends (denoted by the dashed-line box), leaving x naming or mapped to deallocated memory.
part& supplier()
{
static part a = { 'd', 10 };
return a;
}
void client()
{
part& x = supplier();
}
(a)
(b)
part& supplier()
{
part* a = new part {'d', 10 };
cout << &*a << endl;
return *a;
}
void client()
{
part& x = supplier();
cout << &x << endl;
}
(c)
(d)
Correct return-by-reference.
Return-by-reference is difficult to understand and challenging to illustrate. We can often replace it with return-by-value with little effort. However, return-by-reference has one advantage that return-by-value does not. A function returning a value can only form an r-value expression, but one returning a reference can also form an l-value expression. The example demonstrates two ways of implementing return by reference.
Carefully compare this supplier function with Figure 4(a), noticing the addition of the static keyword, preventing the memory deallocation when the function ends and retaining the saved data between function calls. Although the program defines the namesa and x in different scopes, they refer to the same memory location, which remains allocated throughout the program execution.
The solid-line box in this figure replaces the dashed-line box of Figure 4(b), suggesting that object a remains deallocated when function supplier ends.
The two functions demonstrate the effect of pass by reference by creating an object in one function and printing its address in both functions. Understanding the behavior of the functions rests on one crucial observation: variable a is a local stack variable, deallocated when supplier ends. However, the object it points to is allocated on the heap and is not deallocated.
supplier
Creates the object with the new operator, allocating memory on the heap, which remains allocated until the program explicitly deallocates it with the delete operator.
In the cout statement, the dereference operator, *, has a higher precedence than the address-of operator, &, and operates first, getting the object through the pointer. Then, the address-of operator, &, gets its address.
The dereference operator in the return statement causes the function to return a reference to the object, matching the function's return type.
client
The function saves a reference to the object in variable x.
The address-of operator in the cout statement, gets the object's address.
The cout statement, the expression &x gets the address of x, which is also the object's address. It prints the object's address, which is the same address printed in supplier, demonstrating that the object resulting from the dereference operation in supplier is that same object named x in client..
Two views of the new object and the pointer and reference variables. The first illustrates pointer a pointing at the new object. The second, following the function return and assignment operation, illustrates that the program has deallocated a and mapped x to the address of the object form by the dereferencing operation.
Returning Non-Local Data
Every variable defined inside a function is "local data." Therefore, the term "non-local" data is relative to a specific function. The previous examples defined a variable in the supplier function (making it "local" to supplier) and returned it to the client. When returning the variable by pointer or reference, the challenge was avoiding pointing or referring to deallocated memory. The following examples define the variable in client (making it local to client but non-local to supplier), pass it to supplier, which modifies and returns it. Programmers can define functions using various parameter-passing and return mechanisms to provide additional solutions to the problem; nevertheless, the examples use the same parameter-passing and return mechanism in each version for simplicity. The by-value version is more familiar but less efficient and more restrictive.
By-Value
By-Pointer
By-Reference
part supplier(part p)
{
a.type = 'd';
a.id = 10;
return a;
}
void client()
{
part y;
part x = supplier(y);
}
Returning non-local data.
In the first row examples, the client function creates a part object, variable y, and passes it to the supplier. The supplier modifies the object and returns it to the client. The second and third rows illustrate how programs can use the returned object.
Parameter p stores a copy of object y, which the function modifies and returns. The return operator is necessary to get the modified object back to the client function, where the assignment stores it in variable x. Therefore, there are at most two objects at any time in the program.
Parameter p points or refers to y, implementing INOUT passing methods that allow data to flow into and out of the supplier function through the argument/parameter pair, making the return operation unnecessary. Nevertheless, returning a parameter is a convenient and frequently used technique. After the return operation, variable x points to or refers to y, so the program's execution has only one object.
supplier modifies its parameter p and returns a copy of it. The returned object becomes the argument passed to client2.
Although supplier implements an INOUT passing mechanism, the return operator allows programmers to use it as an argument to a second function call.
Functions returning by-value can only operate as an r-value expression.
Returning by-pointer or by-reference allows programmers to use the function call as an l-value (a value appearing on the left side of an assignment operator). Chapter 10 illustrates authentic uses for this feature.