Before I get to the meat, let's cover one slightly special thing about most string classes. String classes are usually implemented as a kind of smart pointer to the string's buffer. This means that:
std::string s1("testing");
std::string s2;
s2 = s1;
Although s2 is a unique string class, after the assignment s2 = s1
, there is still only one string buffer between them. That buffer isn't copied, it is shared in a kind of read only arrangement. If a change is made to the string in s2, at that moment a copy is created so as to make the two strings point to different buffers.
Your question is probably not about the buffers themselves, but the string object which operates those buffers, but it's tangentially related in the case of strings (and, similarly, of std::shared_ptr for similar reasons) where copy performance is concerned. Copying a std::string class is often much less work than copying the underlying buffer.
That said, there's another point regarding your code sample that deserves addressing, and that's what is done with the return values from these functions (in part because you asked what the & does after the string in two of them).
Repeating with slight expansion:
#include <string>
using namespace std;
string t1(string z) { return z; }
string *t2(string &z) { return &z; }
string& t3(string *z) { return *z; }
string& t4(string& z) { return z; }
string t5(string &z) { return z; }
int main() {
string s; string x; string *xp
x = t1(s);
xp = t2(s);
x = t3(&s);
x = t4(s);
x = t5(s);
return 0;
}
Now, it's important to expand on function t1 a moment. There's theory, and there's actual result, which differ in all modern C++ compilers. On an exam I'd expect you'd answer to pure theory, ignoring elided copies, which come into play here. Consider x = t1(s)
, where in theory s is copied as the parameter to the function, at which point z, within the function, is a copy of the s from the caller. The return is by value, so in theory a second copy is created to return. Then, in theory, another copy is performed as x is assigned. Now, that may also be what you witness if you trace through that in the debugger. However, in all but the most naive compilers, all of these copies will be elided, such that x will receive a copy of s as if x = s
were written (and most compilers would examine this literal code, realize nothing is done, and emit a program that does nothing but return).
Now, about x = t2(s);
The parameter is a reference to a string (these things are interpreted from right to left, so think reference to a string even though most speak "string reference". That means there's no copy used by the function, it is the caller's s. This function returns the address of that string, a pointer, which means no copy is made of s - at most we would say a copy of the pointer is returned. This is the same as having written xp = &s;
In x = t3(&s)
we have a curious case. The function accepts a pointer, which requires &s to take the address of s to provide that pointer, and as such no copy of s is made at the function call. The function returns a reference to a string (read just as before, from right to left, though some might say a string reference). Since this is a dereference of a pointer, the result is just referring to s via it's address, and no copy is made in the return. This is further supported by the fact that the return is a reference. References are implemented as a pointer. It's a special kind of pointer, but under the hood, it's a pointer - no copy is made. However, since x is a unique object, a copy is made at the assignment from that reference in assigning x to it. It resolves to the same thing as having written x = s;
There are other usage case this function supports which deserves separate consideration:
string xr( t3( &s ) );
In this case the reference is used to initialize xr (the reference returned from t3). It's similar to string xr( s );
. So far, not a revelation. However, consider using the returned string as compared to t2 and t1.
t1(s).length();
t2(s)->length();
t3(&s).length();
Here, the return from each function is used to call a member of string. The call with t1 has copied s into the function, then copied again to return the temporary string, which is then destroyed (a destructor will be called), which is a point you haven't really addressed in your inquiry.
The call with t2 and t3, however, are actually using s for the call without any copy implied. In the t2 case, however, the call is by pointer. The t2 case is like having written (oddly) (&s)->length(), whereas the t3 case is the same as having written s.length().
T4 is exactly the same thing as t3, only differing in how the call is made and the implication which is associated with the possibility that a nullptr might be passed to t3 (causing a crash at the dereference), which can't happen with t4.
T5 differs from t4 (and t3) only because a copy is implied due to the return by value. What is returned is like t1, operates like t1, and only differs from t1 by implying that t5 does not create a copy for operation with the function body, it just creates a copy for the return.
Assuming the example code you provided, appending main after the call to t5:
string a, b;
// t1 is like having written:
a = s;
b = a;
x = b;
// t5 is like having written:
b = s;
x = b;
Meaning, the first copy of t1 is eliminated by the fact t5 takes a reference instead of a value.
In modern C++ we generally ignore the peformance implication by theory in cases like t1 or t4, t5. We're more concerned with why a reference is used instead of a copy, because the side effect of using a reference is that changes made to the string within the function t5 is made to the caller's s, whereas a copy is implied in t1 and therefore the caller's s is not changed. That is an important component of your question.
Theory will always make a copy where a copy is implied by the writing, as detailed above, but in practice copies are elided (avoided) due to optimization. In the case of t1, for example, that literal code elides all implied copies - no copies would be performed. However, if a change were made to z within the function body of t1, that changes things. If a change is made to t1 the compiler realizes that the side effect of changing z would change s unless a copy is made, which means that one copy implied by the pass by value parameter of t1 would be created, to avoid that side effect, but still elide the copy implied by the return by value.