Moving, “RValue References”¶
“Return Object” Problem: Lifetime (1)¶
Whole class of problems: lifetime of returned objects
const std::string& f() {
std::string s{"blah"};
return s;
}
const std::string& f() {
return "blah";
}
“Return Object” Problem: Lifetime (2)¶
const std::string& f() {
std::string s{"blah"};
return s;
}
Object’s home is on the stack
Returning reference to it
⟶ “undefined behavior”
Fortunately compilers can detect and warn
warning: reference to local variable ‘s’ returned
std::string s{"blah"};
^
“Return Object” Problem: Lifetime (3)¶
const std::string& f() {
return "blah";
}
C string converted to
std::string
to match return typeReturn type being reference is irrelevant
⟶ temporary object
⟶ “undefined behavior”
warning: returning reference to temporary
return "blah";
^
“Return Object” Problem: Lifetime (4)¶
Solution: return by copy
std::string f() {
return "blah";
}
Before return, construct temporary from
"blah"
During return, copy-construct receiver object
After return (during stack frame cleanup), destroy temporary
⟶ Performance
Though
std::string
objects are usually reference counted (but not by standard)⟶ Cheap copy
“Return Object” Problem: Performance¶
std::vector<int> f() {
std::vector<int> v;
int i=100000;
while (i--)
v.push_back(i);
return v;
}
Semantically correct
Perfectly readable
It’s just that arrays of 100000 elements aren’t copied so lightly
Enter Rvalue References
#include <string>
#include <vector>
const std::string& returns_temporary()
{
return "blah";
}
const std::string& returns_stackobject()
{
std::string s{"blah"};
return s;
}
std::string returns_copy()
{
std::string s{"blah"};
return s;
}
void output_parameter(std::string& s)
{
s = "blah";
}
std::vector<int> returns_vector_by_copy()
{
std::vector<int> v;
int i=100000;
while (i--)
v.push_back(i);
return v;
}
int main()
{
std::string s;
s = returns_temporary();
s = returns_stackobject();
s = returns_copy();
output_parameter(s);
auto v = returns_vector_by_copy();
}
Move Semantics: Wish List¶
Wish list:
Copy/assignment as before
Special constructor for moving
Can that be implemented in C++03?
Idea: non-const reference
Exercise
Write a
class X
that carries an array ofint
and implements the usual copy semantics and a proper destructor.Additionally, for performance, the class provides a constructor that transfers ownership of the owned buffer.
Try out the scenarios above, and see what’s to be done in order for the move constructor to (not) be called.
Move Semantics, in C++03¶
Clumsy, isn’t it?
Constructor with non-const reference preferred over const
⟶ Have to be explicit when moving is not wanted - which is the regular case!
#include <iostream>
#include <cstring>
class X
{
public:
X(size_t size)
: data(new int[size]),
size(size)
{
std::cerr << "X(size_t)" << std::endl;
}
X(const X& x)
: X(x.size)
{
std::cerr << "X(const X&)" << std::endl;
memcpy(data, x.data, sizeof(int)*x.size);
}
X(X& x)
: data(x.data),
size(x.size)
{
std::cerr << "X(X&)" << std::endl;
x.data = 0;
x.size = 0;
}
~X()
{
std::cout << "~X(): data=" << data << std::endl;
if (data != 0)
delete[] data;
}
int *data;
size_t size;
};
X f()
{
X x(100);
return x;
}
int main()
{
std::cerr << "*** init from f() (sigh: RVO)" << std::endl;
{
X x = f();
}
std::cerr << "*** init from non-const" << std::endl;
{
X x(100);
X y(x); // non-const -> moved -> x invalid
}
std::cerr << "*** init from const" << std::endl;
{
const X x(100);
X y(x); // const (no other choice)
}
}
In none of these use cases (except for function return) I want moving!
Function return is optimized away ⟶ Return Value Optimization (RVO)
Lvalues and Rvalues (1)¶
int a = 42;
int b = 43;
a = b; // ok
b = a; // ok
a = a * b; // ok
int c = a * b; // ok
a * b = 42; // error, assignment to rvalue
Lvalues and Rvalues (2)¶
Rules …
Everything that has a name is an Lvalue
Everything that I can assign to is an Lvalue
Everything that I can take the address of is an Lvalue
Everything else is an Rvalue
So …
Temporaries are clearly Rvalues
As are function calls
Moving (1)¶
To make the long story short …
|
struct X
{
X(X&& x)
: data(x.data),
size(x.size)
{
x.data = 0;
x.size = 0;
}
int *data;
size_t size;
};
|
Moving (2)¶
Compiler will DWIM …
Return “by copy”
Select
X(X&&)
Or RVO with copy ctor
X f() { return X{"abc"}; } X x = f();
Ordinary initialization
Select
X(const X&)
X x{"abc"}; X y = x;
Moving (3)¶
Explicitly requesting move operation
X y = std::move(x);
std::move
does not do anything the CPU must knowCasts to
&&
to force selection of move-ctorUsage:
std::sort
, for exampleRearrange items
⟶ Copy or move, depending on what’s there
No C++ Without Pitfalls¶
Compiler selects function based upon parameter type
Normal overload selection
Once called, the parameter is an lvalue
Careful with moving
Bad X(X&& x)
: s_(x.s_) {}
|
Good X(X&& x)
: s_(std::move(x.s_)) {}
|