CS 70

How Functions Return Result Values

So far, we've learned that when we write code like

int x = (a+b)+c;
std::cout << sin(3.14);
std::cout << myString.substr(3,7);

what's really happening behind the scenes is equivalent to

{
    int _temp1 = a+b;
    int x = _temp1+c;
}
{
    double _temp2 = sin(3.14);
    std::cout << _temp2;
}
{
    std::string _temp3 = myString.substr(3,7)
    std::cout << _temp3;
}

In other words, at most one function call happens at a time, and intermediate results are stored in hidden temporary variables.

Thus, we've learned something interesting: every function call that returns a value is actually initializing a new object in memory. (It's either running a constructor to make an instance of a class, or copy initializing a primitive type.)

Thus, for a function call that returns a value,

  • The caller will have allocated space somewhere in its stack frame to store the result.
  • Behind the scenes, the caller tells the called function (a.k.a. “the callee”) where that space is.
  • The called function then initializes that space (usually when it returns).
  • The caller is responsible for destroying the result and eventually deallocating it.

The called function does not know what name, if any, the caller has for the memory it is initializing. It also doesn't know if it's an unnamed temporary location or a named variable. When it makes sense to think of the called function needing a name for the destination memory location in our diagrams, we'll give it the name _retval.

One cool thing about the fact that return is actually initializing an object in the caller is that, although we can say return val;, which copy initializes the object, we can also write return {}; to cause the object to be default initialized or return {arg1, arg2, arg3}; to call a specific constructor!

An Example

The code below shows some of these principles in action:

Cow makeCow(const std::string& name, int cowYears) {
    // the first cow year is equal to 14 human years; each subsequent cow year
    // equals 4 human years

    double humanYears;
    if (cowYears < 14) {
        humanYears = cowYears / 14.0;
    } else {
        humanYears = 1.0 + (cowYears - 14) / 4.0;
    }

    cout << ">> name = " << name << ", at location " << &name << endl;
    cout << ">> cowYears = " << cowYears << ", at location " << &cowYears << endl;
    cout << ">> humanYears = " << humanYears << ", at location " << &humanYears << endl;

    return {name, humanYears};  // Equivalent to: return Cow{name, humanYars};
}

int main() {
    int theAge = 17;
    string theName{"Bessie"};

    cout << "> theAge = " << theAge << ", at location " << &theAge << endl;
    cout << "> theName = '" << theName << "', at location " << &theName << endl;

    cout << makeCow(theName, theAge).age_ << endl;

    cout << "> (the temporary Cow is gone, but main isn't quite done)" << endl;

    return 0;
}

(We've skipped code to include the Cow class, and also some code that causes functions to print a message on entry and exit.)

If we run this code, we see the following output:

In body of main:
> theAge = 17, at location s1
> theName = 'Bessie', at location s2
In body of makeCow:
>> name = Bessie, at location s2
>> cowYears = 17, at location s4
>> humanYears = 1.75, at location s5
+ Created Cow Bessie (via parameterized constructor), at location s3
Leaving makeCow.
1.75
- Destroyed Cow Bessie, at location s3
> (the temporary Cow is gone, but main isn't quite done)
Leaving main.

We can observe a few things here:

  • In makeCow, the name variable is just another name for the theName variable in main, because the name parameter is passed as a reference.
  • In makeCow, the function constructs its resulting Cow value in s3, which belongs to main. (makeCow's first stack slot is s4.)
  • The temporary object for makeCow's result only lasts for the lifetime of the line of code where it appears.
  • Horse speaking

    Hay! How did you get a trace with s1, s2, and so on? Did you hand trace the whole thing with a memory diagram?

  • LHS Cow speaking

    I could have, but it would have taken several minutes to “run” all this code by hand. Instead, I just compiled and ran the C++ program. On a real machine, it outputs the actual pointer values as hexadecimal addresses, so I converted those numbers into symbols in the style of our memory diagrams.

  • Rabbit speaking

    I know you well enough to know you didn't do it by hand! You wrote a Python script to do the conversion!

  • LHS Cow speaking

    *Pretends not to hear.*

So we can see that makeCow really is making a temporary object in main's stack frame.

Drawing a Quick Memory Diagram

The trace above has enough information for us to draw a memory diagram of the code at a specific moment in time. Use the trace above to draw a memory diagram at the point just after we've created the Cow called "Bessie", before we leave makeCow.

When you draw the diagram, also label s3 with the name _retval (an internal name makeCow could use to refer to this location) and temp1_ (an internal name main could use).

Did you draw the diagram?

This video (approximately two minutes long) follows the execution of the code and shows the construction of the diagram. (It actually continues to the point where the makeCow function returns.)

Variation #1

If we change makeCow to take cowYears by constant reference,

Cow makeCow(const std::string& name, const int& cowYears)

we get the following output:

In body of main:
> theAge = 17, at location s1
> theName = 'Bessie', at location s2
In body of makeCow:
>> name = Bessie, at location s2
>> cowYears = 17, at location s1
>> humanYears = 1.75, at location s4
+ Created Cow Bessie (via parameterized constructor), at location s3
Leaving makeCow.
1.75
- Destroyed Cow Bessie, at location s3
> (the temporary Cow is gone, but main isn't quite done)
Leaving main.

Notice that now

  • cowYears is just another name for theAge.
  • humanYears has moved up from s5 to s4.

Variation #2

Now let's revert that change, and change main to

int main() {
    int theAge = 17;
    string theName{"Bessie"};

    cout << "> theAge = " << theAge << ", at location " << &theAge << endl;
    cout << "> theName = '" << theName << "', at location " << &theName << endl;

    Cow theCow = makeCow(theName, theAge);

    cout << "> theCow exists now, at location " << &theCow << endl;

    cout << makeCow("Mabel", theAge+2).age_ << endl;

    Cow theOtherCow = theCow;

    return 0;
}

This version produces the output

In body of main:
> theAge = 17, at location s1
> theName = 'Bessie', at location s2
In body of makeCow:
>> name = Bessie, at location s2
>> cowYears = 17, at location s7
>> humanYears = 1.75, at location s8
+ Created Cow Bessie (via parameterized constructor), at location s3
Leaving makeCow.
> theCow exists now, at location s3
In body of makeCow:
>> name = Mabel, at location s5
>> cowYears = 19, at location s7
>> humanYears = 2.25, at location s8
+ Created Cow Mabel (via parameterized constructor), at location s4
Leaving makeCow.
2.25
- Destroyed Cow Mabel, at location s4
+ Created Cow Copy of Bessie (copying Cow Bessie from s3), at location s6
- Destroyed Cow Copy of Bessie, at location s6
- Destroyed Cow Bessie, at location s3
Leaving main.
  • Duck speaking

    Hang on, aren't both these lines—

    Cow theCow = makeCow(theName, theAge);
    Cow theOtherCow = theCow;
    

    —calls to the copy constructor? Why do we only see one copy being made?

  • LHS Cow speaking

    It's totally fine to think there should be two copies. Because that is what our code says.

  • Bjarne speaking

    But C++ has some rules for avoiding pointless, redundant copying. Putting the result of makeCow into a temporary variable and then copying that temporary variable into theCow would be pointless, redundant copying, so C++ makes it so that makeCow writes its result into the memory for theCow.

  • Goat speaking

    How much of this do we need to actually care about?

  • LHS Cow speaking

    Okay, let's summarize the key takeaways.

Takeaways

When a function returns a value, it actually constructs that value in the calling function. If you're drawing a diagram, you can refer to the space where you'll write the result using the name _retval.

  • Cat speaking

    So, if we have a function that returns an Elephant, it has to deal with all the work of making an elephant.

  • Horse speaking

    Hay! I see it now, this is just like passing-by-value. We're copying data.

  • LHS Cow speaking

    Exactly.

  • Duck speaking

    So if we can pass-by-reference can we return by reference?

  • LHS Cow speaking

    Good question!

(When logged in, completion status appears here.)