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++, 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++, 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”.)

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? 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?

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 in our Docker image:

cs70 DOCKER > clang++ -o model-example model-example.cpp
cs70 DOCKER > ./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 DOCKER > clang++ -O -o model-example model-example.cpp
cs70 DOCKER > ./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.

(When logged in, completion status appears here.)