This is part one of a series of blog posts on temporaries, copies, return value optimization, and passing by value vs. reference.
A good place to start, and the point of this first article, is how a prvalue isn’t necessarily a temporary.
If you’re entirely new to value categories (lvalues, rvalues etc.), you might want to read lvalues, rvalues, glvalues, prvalues, xvalues, help! first.
lvalues vs rvalues
An lvalue is an expression that can not be moved from. An rvalue is an expression that can be moved from.
Let’s first have a look at lvalues. Given this variable v:
std::vector<int> v{1,2,3};
If I now write the expression v somewhere, v is referring to an actual v…
This is part one of a series of blog posts on temporaries, copies, return value optimization, and passing by value vs. reference.
A good place to start, and the point of this first article, is how a prvalue isn’t necessarily a temporary.
If you’re entirely new to value categories (lvalues, rvalues etc.), you might want to read lvalues, rvalues, glvalues, prvalues, xvalues, help! first.
lvalues vs rvalues
An lvalue is an expression that can not be moved from. An rvalue is an expression that can be moved from.
Let’s first have a look at lvalues. Given this variable v:
std::vector<int> v{1,2,3};
If I now write the expression v somewhere, v is referring to an actual variable. I can’t just move from it, as it would mess up an existing object that someone else could still be using. We call an expression like this an lvalue.
For instance, if I pass my existing vector to a function useVector:
useVector(v);
Here, the expression v is an lvalue, and useVector can’t move from it. After all, someone might want to keep using v on a following line.
But if I know I won’t be needing v anymore, I can turn it into an rvalue, by wrapping it in std::move:
useVector(std::move(v));
Here, the expression std::move(v) is an rvalue. useVector would now be allowed to move from v. (And I must take care to not use v again, since it might have been moved into useVector.)
rvalues: xvalues vs prvalues
Here’s another way to get an rvalue:
useVector(std::vector{1,2,3});
Here, the expression std::vector{1,2,3} is also an rvalue, and again useVector would be allowed to move from it.
Notice, however, that these are two different types of rvalues. std::move(v) takes an existing object and casts it to an rvalue. That type of rvalue is called an xvalue, or “eXpiring lvalue”.
On the other hand, std::vector{1,2,3} is a prvalue, or “pure rvalue”. Unlike an xvalue, this expression never referred to an existing object in the first place. People sometimes call this “a temporary”, but, as is the main point of this article, that’s not necessarily true.
A prvalue in itself is not a temporary. A prvalue is not an object. A prvalue just represents “the idea of the object”, and only turns into a temporary when it absolutely needs to. For instance:
std::vector v = std::vector{1,2,3};
Here, the prvalue std::vector{1,2,3} does not turn into a temporary that is then used to initialize v. Rather, the prvalue is used to initialize v directly, just as if you’d written std::vector v{1,2,3};. No extra temporary is created, and no copies or moves are performed.
Similarly:
void useVector(std::vector<int> v);
useVector(std::vector{1,2,3});
Here, useVector takes its parameter by value. std::vector{1,2,3} never turns into a temporary, the prvalue expression is instead used to initialize the parameter v directly, just like in the previous example. No extra temporary is created, and no copies of moves are performed.
Temporary materialization
However, if useVector takes its parameter by reference:
void useVector(const std::vector<int>& v);
useVector(std::vector{1,2,3});
Now, the reference parameter v needs some object to bind to, and std::vector{1,2,3} actually turns into a temporary object that v can bind to. This is called “temporary materialization”.
Return values
A function call that returns by value is also a prvalue 1. So, given this definition of getVector() and a call to it:
std::vector<int> getVector();
std::vector<int> v = getVector();
Here, the call getVector() is a prvalue which initializes v directly. There is no temporary that is then copied/moved into v. (There might be a copy involved in the return statement inside getVector(), but that’s a story for the next article.)
Conclusion
The point is that a prvalue only materializes into a temporary as a very last resort, avoiding unnecessary copies or moves. Until it needs to materialize, it only represents “the idea of the object”, i.e. what the object would be when it materializes into a temporary or is used to initialize something.
It is important to note that this has nothing to do with optimization. There is no temporary std::vector to optimize away in the first place, there’s just the prvalue, just the “idea of the object”. And then that idea of an object can materialize into an actual object if needed.
Footnotes
- §expr.call¶13: “A function call is an lvalue if the result type is an lvalue reference type or an rvalue reference to function type, an xvalue if the result type is an rvalue reference to object type, and a prvalue otherwise.” ↩︎
Discover more from C++ on a Friday
Subscribe to get the latest posts sent to your email.