Rvalue reference and their use

这是Laurent Kneip教授的CS133的第7讲,上课的时候听的迷迷糊糊的,但是后来写作业的时候觉得这些内容非常重要但是不容易理解,所以写一点总结。

motivation

我们想要让一个class来代替programmer来管理动态分配的memory,在constructor里面分配资源,在deconstructor里面de-allocate.

Smart pointer:

1
2
3
4
5
6
7
8
9
10
11
12
template <class T>
class Auto_ptr1 {
T* m_ptr;
public:
Auto_ptr1(T* ptr=nullptr) : m_ptr(ptr) {}
~Auto_ptr1() {delete m_ptr;}

// overload deference and operator ->
// let us use Auto_ptr like m_ptr
T& operator*() const {return *m_ptr;}
T* operator->() const {return m_ptr;}
}

现在我们就可以愉快地用Smart pointer了,比如:

1
2
3
4
5
6
7
8
9
void function() {
Auto_ptr<Resource> ptr(new Resource);
int x = somevalue;
if (x == 0) {
return; // 不必担心内存泄漏
} else {
ptr->sayHi();
}
}

Auto-pointer flaw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main() {
Auto_ptr1<Resource> res1(new Resource);
Auto_ptr1<Resource> res2(res1); // both res1 and res2 are pointing to
// at the same resource

return 0;
}
or
int main() {
Auto_ptr1<Resource> res1(new Resource);
Auto_ptr1<Resource> res2;
res2 = res1;

return 0;
}
// Resource acquired
// Resource destroyed
// Segmentation fault

similar problem is caused by this:

1
2
3
4
5
6
7
8
9
void passbyvalue(Auto_ptr1<Resource> res) {}

int main() {
Auto_ptr1<Resource> res1(new Resource);
//pass by value will make a shallow copy of res1 and then destroy this copy upon finishing the function body
passbyvalue(res1);
return 0;
}
// crash

solutions to the Auto_ptr flaw

  • solution 1: prevent the copy constructor and assignment operator to be "available"

    • method 1: explicitly declare copy constructor and make them private
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template <class T>
    class Auto_ptr1 {
    T* m_ptr;
    public:
    Auto_ptr1(T* ptr=nullptr) : m_ptr(ptr) {}
    ~Auto_ptr1() {delete m_ptr;}
    // ...
    private:
    Auto_ptr(const Auto_ptr1 &);
    Auto_ptr& operator= (const Auto_ptr1 &);
    }

    problem: less efficient than the default constructor, member functions and friends can still call the private defined constructors, unclear

    • method 2: the C++11 way
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template <class T>
    class Auto_ptr1 {
    T* m_ptr;
    public:
    Auto_ptr1(T* ptr=nullptr) : m_ptr(ptr) {}
    ~Auto_ptr1() {delete m_ptr;}
    // ...
    Auto_ptr(const Auto_ptr1 &) = delete;
    Auto_ptr& operator= (const Auto_ptr1 &) = delete;
    }

    pass by value is no longer available -> we can just pass reference

    however, we can no longer do this:

    1
    2
    3
    4
    5
    6
    ??? generateResource() {
    Resource *r = new Resource;
    return Auto_ptr1(r);
    }
    // can't return by reference because object will be destroyed
    // can't return by value because copy-constructor is disabled

move semantics

我们不想copy value, just move ownership

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template <class T>
class Auto_ptr2 {
T* m_ptr;
public:
Auto_ptr2(T* ptr=nullptr) : m_ptr(ptr) {}
~Auto_ptr2() {delete m_ptr;}
// ...
T& operator*() const {return *m_ptr;}
T* operator->() const {return m_ptr;}
bool isNull() const {return m_ptr == nullptr;}

// something new here
// copy constructor with move semantics
Auto_ptr2(Auto_ptr2 &a) { // no longer const
m_ptr = a.ptr;
a.ptr = nullptr;
}
Auto_ptr2& operator= (Auto_ptr2 &a) {
if (&a == this) return *this;
delete m_ptr;
m_ptr = a.m_ptr;
a.m_ptr = nullptr;
return *this;
}
}
  • std::auto_ptr is implemented exactly like this in original C++98 standard
  • problem occurs if pass by value
  • removed since C++17

Lvalues and Rvalues

general rule: if you can take its address, it's an lvalue, else, it's an rvalue

some examples:

1
2
3
4
template<typename T1, typename T2>
int sizeDiff(const T1 &c1, const T2 &c2) {
return c1.size() - c2.size();
}

lvalue: sizeDiff, c1, c2

rvalue: return value


1
2
3
4
5
int *px;
std::vector<int> v;
std::unordered_set<int> s;
...
*px = sizeDiff(v, s);

lvalue: px, v, s

rvalue: sizeDiff(v, s)


1
std::ifstream myinput(std::string("something"));

anonymous objects are rvalues


  • Rvalues die at the end of an expression
    • they can't be assigned to

but there is a special case:

1
2
3
4
5
6
7
8
void printSomething(const std::string &str) {
// the parameter is an lvalue(object with name and space)
std::cout << str << std::endl;
}
int main() {
printSomething(std::string("hello")); // we passed in an rvalue!!!
return 0;
}

local const reference can be used to prolong the lifetime of temporary value and refers to it until the end of the containing scope(does not incur the cost of a copy-construction)

Rvalue references

1
2
3
int x = 5;
int &lref = x; // lvalue reference initialized with lvalue x
int &&rref = 5; // rvalue reference initialized with rvalue 5
  • rvalue reference allows us to
    • extend the lifespan of the (rvalue) object by the lifespan of the rvalue reference
    • modify the rvalue

move construction and move assignment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
template<class T>
class Auto_ptr3 {
T* m_ptr;
public:
Auto_ptr3(T* ptr=nullptr) : m_ptr(ptr) {}
~Auto_ptr3() {delete m_ptr;}

// copy constructor: do deep copy of a.m_ptr to m_ptr
Auto_ptr3(const Auto_ptr3 &a) {
m_ptr = new T;
*m_ptr = *a.m_ptr;
}
// copy assignment: do deep copy of a.m_ptr to m_ptr
Auto_ptr3& operator=(const Auto_ptr3& a) {
if (&a == this) return *this;

delete m_ptr;
m_ptr = new T;
*m_ptr = *a.m_ptr;
return *this;
}

T& operator*() const {return *m_ptr;}
T* operator->() const {return m_ptr;}
bool isNull() const {return m_ptr == nullptr;}
};

the usage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Resource {
public:
Resource() {std::cout << "resource acquired\n";}
~Resource() {std::cout << "resource destroyed\n";}
};
Auto_ptr3<Resource> generateResource() {
Auto_ptr3<Resource> res(new Resource);
return res; // return value invokes copy constructor
}
int main() {
Auto_ptr3<Resource> mainres; // no output until now
mainres = generateResource(); // generateResource() print 1, copy assignment print 1
return 0;
}

output:

1
2
3
4
resource acquired
resource acquired
resource destroyed
resource destroyed

2 allocations to create an object, inefficient but very safe


  • move construction/assignment
    • role: move ownership from one obj to another
    • use instead of copying to gain efficiency
    • similar to regular copy constructor/assignment oprator
      • take non-const rvalue reference instead of const lvalue reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
template<class T>
class Auto_ptr4 {
T* m_ptr;
public:
Auto_ptr4(T* ptr=nullptr) : m_ptr(ptr) {}
~Auto_ptr4() {delete m_ptr;}

// copy constructor: do deep copy of a.m_ptr to m_ptr
Auto_ptr3(const Auto_ptr3 &a) {
m_ptr = new T;
*m_ptr = *a.m_ptr;
}
// copy assignment: do deep copy of a.m_ptr to m_ptr
Auto_ptr3& operator=(const Auto_ptr3& a) {
if (&a == this) return *this;
delete m_ptr;
m_ptr = new T;
*m_ptr = *a.m_ptr;
return *this;
}
// move constructor: transfer ownership of a.m_ptr to m_ptr
Auto_ptr4(Auto_ptr&& a) : m_ptr(a.m_ptr) {
a.m_ptr = nullptr;
// remove ownership
// prevent a from destroying resource
// prevent a from causing dangling pointers
}
// move assignment operator: transfer ownership of a.m_ptr to m_ptr
Auto_ptr4& operator=(Auto_ptr4&& a) {
// self-assignment detection
if (&a == this) return *this;

delete m_ptr;
m_ptr = a.m_ptr;
a.m_ptr = nullptr; // remove ownership, prevent a from destroying resource, prevent a from causing dangling pointers
return *this;
}



T& operator*() const {return *m_ptr;}
T* operator->() const {return m_ptr;}
bool isNull() const {return m_ptr == nullptr;}
};

and now we run the program again:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Resource {
public:
Resource() {std::cout << "resource acquired\n";}
~Resource() {std::cout << "resource destroyed\n";}
};
Auto_ptr3<Resource> generateResource() {
Auto_ptr3<Resource> res(new Resource);
return res; // return value invokes copy constructor
}
int main() {
Auto_ptr3<Resource> mainres; // no output until now
mainres = generateResource(); // generateResource() print 1, copy assignment print 1
return 0;
}

output:

1
2
resource acquired
resource destroyed
  • key insights
    • if an lvalue is used, like: a = b(we should make a deep copy and not alter b)
    • if an rvalue is used, like: a = b + c(rvalue is about to be destroyed, it is reasonale to steal the ownership and avoid copying)

std::move

extension of move semantics to lvalues!

1
2
3
4
5
6
template<class T>
void swap(T& a, T& b) {
T tmp{a}; // invokes copy constructor
a = b; // invokes copy assignment
b = tmp; // invokes copy assignment
}

however doing 3 copies is unnecessary, we can do 3 moves instead

solution: cast the lvalues to rvalues with std::move()

1
2
3
4
5
6
template<class T>
void swap(T& a, T& b) {
T tmp {std::move(a)}; // invokes move constructor
a = std::move(b); // invokes move assignment
b = std::move(tmp); // invokes move assignment
}

what happens to the lvalue !?

一个小实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <vector>
#include <string>
using namespace std;

int main() {
vector<string> vec;
string str = "Knock";
cout << "copying str\n";

vec.push_back(str);
cout << "str is:" << str << "and vec[0] is:" << vec[0] << endl;

cout << "move str\n";

vec.push_back(std::move(str));
cout << "str is:" << str << "vec[0] is:" << vec[0] << "vec[1] is:" << vec[1] << endl;

return 0;
}

output:

1
2
3
4
copying str
str is:Knockand vec[0] is:Knock
move str
str is:vec[0] is:Knockvec[1] is:Knock
  • objects that are being stolen from need to be left in a defined "null state"

  • they are not a temporary after all, and can be used again later