Value return semantics
The seven rules in this section are in accordance with the previously mentioned rule “F.20: For ‘out’ output values, prefer return values to output parameters.” The rules of this section are, in particular, about special use cases and don’ts.
When to return a pointer (T*) or an lvalue reference (T&)
As we know from the last section (Parameter Passing: Ownership Semantics), a pointer or a reference should never transfer ownership.
A pointer should indicate only a position. This is exactly what the function find does.
Node* find(Node* t, const string& s) { if (!t || t->name == s) return t; if ((auto p = find(t->left, s))) return p; if ((auto p = find(t->right, s))) return p; return nullptr; }
The pointer indicates that the Node is holding the position of s.
When return no object is not an option, using a reference instead of a pointer comes into play.
Sometimes you want to chain operations without unnecessary copying and destruction of temporaries. Typical use cases are input and output streams or assignment operators (“F.47: Return T& from assignment operators”). What is the subtle difference between returning by T& or returning by T in the following code snippet?
A& operator = (const A& rhs) { ... }; A operator = (const A& rhs) { ... }; A = a1, a2, a3; a1 = a2 = a3;
The copy assignment operator returning a copy (A) triggers the creation of two additional temporary objects of type A.
A reference to a local
Returning a reference (pointer) to a local is undefined behavior.
Undefined behavior essentially means this: Don’t make any assumptions about your program. Fix undefined behavior. The program lambdaFunctionCapture.cpp returns a reference to a local.
// lambdaFunctionCapture.cpp #include <functional> #include <iostream> #include <string> auto makeLambda() { const std::string val = "on stack created"; return [&val]{return val;}; // (2) } int main() { auto bad = makeLambda(); // (1) std::cout << bad(); // (3) }
The main function calls the function makeLambda() (1). The function returns a lambda expression, which has a reference to the local variable val (2).
The call bad() (3) causes the undefined behavior because the lambda expression uses a reference to the local val. As local, its lifetime ends with the scope of makeLambda().
Executing the program gives unpredictable results. Sometimes I get the entire string, sometimes a part of the string, or sometimes just the value 0. As an example, here are two runs of the program.
In the first run, arbitrary characters are displayed until the string terminating symbol (\0) ends it (see Figure 4.4).
Figure 4.4 Displaying arbitrary characters
In the second run, the program causes a core dump (see Figure 4.5).
Figure 4.5 Causing a core dump
and
Both rules are very rigorous.
T&&
You should not use a T&& as a return type. Here is a small example to demonstrate the issue.
// returnRvalueReference.cpp int&& returnRvalueReference() { return int{}; } int main() { auto myInt = returnRvalueReference(); }
When compiled, the GCCcompiler complains immediately about a reference to a temporary (see Figure 4.6). To be precise, the lifetime of the temporary ends with the end of the full expression auto myInt = returnRvalueReference();.
Figure 4.6 Returning a reference to a temporary
std::move(local)
Thanks to copy elision with RVO and NRVO, using return std::move(local) is not an optimization but a pessimization. Pessimization means that your program may become slower.
According to the C++ standard, there are two variations of the main function:
int main() { ... } int main(int argc, char** argv) { ... }
The second version is equivalent to int main(int argc, char* argv[]) { ... }.
The main function does not need a return statement. If control reaches the end of the main function without encountering a return statement, the effect is that of executing return 0;. return 0 stands for the successful execution of the program.