CS 70

Discovering Temporaries

  • LHS Cow speaking

    So far, everything should have pretty much been review, but hopefully giving you more practice with the concepts.

  • RHS Cow speaking

    But now we'll push the boundaries a bit to see whether our conceptual model works or needs more refinement.

If you closed it

and fork it.

Results from Expressions

Our next example to try is

int main() {
    int x = 23;
    int y = 19;
    cout << "main's x is at address " << &x << ", and holds " << x << endl;
    cout << "main's y is at address " << &y << ", and holds " << y << endl;

    passByVal(x + y);

    return 0;
}

If we hand-simulate it, it will produce

main's x is at address s1, and holds 23
main's y is at address s2, and holds 19
passByVal's v is at address s3, and holds 42

and you can check those results by running our code in the online compiler.

But now let's try a little experiment. We'll change the code to

int main() {
    int x = 23;
    int y = 19;
    cout << "main's x is at address " << &x << ", and holds " << x << endl;
    cout << "main's y is at address " << &y << ", and holds " << y << endl;

    int* where = &(x + y);

    return 0;
}

Think about whether that highlighed line makes sense. We're going to see what happens in the online compiler in a moment, but before we do, what do you think?

Does &(x+y) make sense?

In the online compiler, change the code to match the code above and try to run it (which will cause it to try to compile it). If you've copied the code correctly, you'll get a specific (and probably hard to understand) error message.

Paste the compiler error.

When I try to compile the code with clang++ on the CS 70 server, I get the error

main.cpp:19:18: error: cannot take the address of an rvalue of type 'int'
    int* where = &(x + y);
                 ^ ~~~~~
1 error generated.

and if I use a different compiler, g++ (in online GDB), I get a similar but different error:

main.cpp: In function 'int main()':
main.cpp:19:21: error: lvalue required as unary '&' operand
   19 |     int* where = &(x+y);
      |                   ~~^~~
  • Hedgehog speaking

    Argh! What's an “rvalue”? Or an “lvalue”, for that matter?

Terminology:

  • An lvalue can be thought of something like a variable that has an associated memory Location.

  • An rvalue can be thought of as a “result” value, or a tempoRary value.

  • Horse speaking

    Hay! It looks to me like there's some kind of left/right thing going on.

  • Bjarne speaking

    Yes. If we think of x = y + z, the thing on the left needs to be something that has a location in memory, so we can write in a new value, and the thing on the right is the result of an expression. It wouldn't make sense to say y + z = x, so the lefthand side and the righthand side are different sorts of thing.

  • LHS Cow speaking

    These names are a bit arcane, but knowing them helps us understand compiler error messages!

So, for right now, it seems like the results of expressions, like x+y, don't have a location in memory.

If we replace the line with

int& value = x+y;

we also get errors (different ones!). clang++ says

main.cpp:19:10: error: non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
    int& value = x+y;
         ^       ~~~
1 error generated.

and g++ says

main.cpp: In function 'int main()':
main.cpp:19:19: error: cannot bind non-const lvalue reference of type int&' to an rvalue of type 'int'
   19 |     int& value = x+y;
      |                  ~^~

(Notice that this time g++ describes x+y as “an rvalue” and clang++ calls it “a temporary”—they mean the exact same thing, but this is why it's good to know both terms!)

With this compiler feedback in mind, do you think our next example will work?

int main() {
    int x = 23;
    int y = 19;
    cout << "main's x is at address " << &x << ", and holds " << x << endl;
    cout << "main's y is at address " << &y << ", and holds " << y << endl;

    passByRef(x + y);

    return 0;
}

Take a guess, do you think we can pass x+y into passByRef()?

Moment of Truth: What Actually Happens?

Now try the change in the online compiler: change the code to match the example above and try to compile the file.

What happened when you tried to compile and run the code in OnlineGDB? Can you guess at an explanation?

It works! And the output is basically saying

main's x is at address s1, and holds 23
main's y is at address s2, and holds 19
passByRef's r is at address s3, and holds 42

So it turns out that main has three stack slots, not two, and the third one is holding the result of x+y. So we could say that x+y does have a location in memory.

The Truth about Temporaries

Whenever C++ invokes any kind of function (or operator) that returns a value, or we directly invoke a constructor (like we did in the Before You Start section), if we're not putting the resulting value directly into a known memory location (such as a named variable), the program stores it as a “temporary” value. The returned value is, in essence, a short-lived, unnamed variable that exists only for the duration of that line of code.

Much of the time, we can just ignore the fact that temporaries exist. But sometimes it is useful to know that they do. When we want to model temporaries in our diagrams, we'll give them fake names like _temp1, _temp2, and so on.

So we can model a line like

passByRef(x+y);

as if it were

{
    int _temp1 = x+y;
    passByRef(_temp1);
}
  • Duck speaking

    If temporaries exist, why can't I say &(x+y)?

  • LHS Cow speaking

    Mostly to stop people who are super confused from making mistakes. Our passByRef function showed that we can actually find out where in memory this intermediate result is stored.

Time for You to Experiment!

One reason we use the online compiler is to allow you to try some things without having a build environment ready to use. Now's your chance! Here are some ideas:

  • See if memory gets reused for temporaries by trying
passByRef(x+y);
passByRef(x*y);
  • See if const matters by removing the const from passByRef or adding it somewhere else; for example,
const int& v = x+y;
cout << "main's v is at address " << &v << ", and holds " << v << endl;

What did you try? What's something you figured out?

Storage Reuse for Temporaries

FWIW, if you tried

    passByRef(x+y);
    passByRef(x*y);

you might think you know what's happening, but the truth is that “it depends”, and that's a good thing to remember about any experiment you try. You might get some insights, but it may also be that you're not seeing the full picture. For example, here's that code example compiled two different ways on the CS 70 server:

cs70 SERVER > clang++ -o model-example model-example.cpp
cs70 SERVER > ./model-example
main's x is at address 0x7ffcceef80f8, and holds 23
main's y is at address 0x7ffcceef80f4, and holds 19
passByRef's r is at address 0x7ffcceef80f0, and holds 42
passByRef's r is at address 0x7ffcceef80ec, and holds 437

cs70 SERVER > clang++ -O -o model-example model-example.cpp
cs70 SERVER > ./model-example
main's x is at address 0x7ffeb2691c90, and holds 23
main's y is at address 0x7ffeb2691c8c, and holds 19
passByRef's r is at address 0x7ffeb2691c94, and holds 42
passByRef's r is at address 0x7ffeb2691c94, and holds 437

In the first example, we compiled without optimization and the space for the temporary wasn't reused, but in the second case, where we compiled with (some) optimization (-O), the space was reused.

Only const References can Bind to Temporaries

If you tried messing with const, you might have discovered that if you change passByRef to take a non-const reference, like this:

void passByRef(int& r) {
    cout << "passByRef's r is at address " << &r << ", and holds " << r << endl;
}

then the code won't compile. The compiler will give you an error message like

main.cpp:19:19: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int' 19 | passByRef(x+y); | ~^~

This is one case of C++ trying to help you avoid mistakes. If you could bind a non-const reference to a temporary, you could then try to modify the temporary through that reference, which would be very confusing since the temporary is about to go away. In most situations, it would be a strange thing to do to go to a lot of effort to make updates to an object only to turn around and throw away all that work immediately afterwards.

  • Hedgehog speaking

    So I get that temporaries exist, but do I need to draw out every temporary in my memory diagrams?

  • LHS Cow speaking

    Most of the time, no. If you're just trying to understand a piece of code, you can usually ignore temporaries. But in a case like passByRef(x+y), we've written code that specifically interacts with a temporary, so we need to include it in our diagram.

  • RHS Cow speaking

    Remember that the goal of memory diagrams is to help you understand what's going on in your code. If you can do that without drawing every temporary, great! But if you need to include them to understand something, go ahead and do so.

(When logged in, completion status appears here.)