CS 105C: Lecture 9

Last Time...

Copying is expensive, stealing is cheap!*

int main(){
  X x, x2;
  ...
  x2 = create_an_x();
  x = x2;
}

Wherever possible, we'd like to move data around instead of making copies of it.

*Please do not attempt to use this as a defense in court.

But we can't always steal: stealing the result of create_an_x() is okay, while stealing x2 is not okay.

Rvalues are about to be lost anyways

Anything which must go on the right hand side of an assignment operator cannot be reused unless we bind its name.

string s1, s2;
...
interleave(s1, s2);

Unless we assign this to something, the return value from interleave() will expire after this line is executed.

Use rvalue references, which can only bind to rvalues

Can I

bind a...

...to a

Lvalue Reference

const Lvalue Reference

Rvalue Reference

Lvalue

Rvalue

int y;
int &x = y;
int y;
const int &x = y;
int y;
int &&x = y;

int &x = 5;

const int &x = 5;

int &&x = 5;

Now, instead of making copies all the time, we can steal data when we know the other object is about to expire!

// Expensive to Copy!
struct ETC{
  int* data;
  int size;
  ETC() = something;
  ETC(const ETC& other) = something;
  ETC& operator=(const ETC& other) = something;
  ETC(ETC&& other) noexcept : data{nullptr}, size{0} {
    std::swap(data, other.data);
    std::swap(size, other.size);
  }
  ETC& operator=(ETC&& other) noexcept {
    std::swap(data, other.data);
    std::swap(size, other.size);
  }
};

Use looks exactly like before, but with +efficiency

ETC gen_etc();

int main(){
  ETC e1 = gen_etc();  // Constructed by move
  ETC e2 = e1;         // Constructed by copy
}

Points of Confusion

Use std::move to force move construction.

ETC a1;
...
ETC a2 = a1; // Copied
ETC a3 = std::move(a1); // Moved

But std::move doesn't actually move anything! It just converts its operand into an rvalue reference

After move, programmer may not assume anything about the state of a1.

A named rvalue reference is an lvalue!

ETC&& a = gen_etc();
ETC b = a; // Constructed by copy!

Rvalues are about to be lost, so we can steal all their stuff--once you bind to an rvalue reference, the rvalue's lifetime is extended, so this is no longer the case.

CS 105C: Lecture 9

Smart Pointers and Perfect Forwarding

But first, a redux!

RValues and LValues

The Ownership Problem

Episode 5: RAII Strikes Back!

Let's write an RAII wrapper class!

Last time we discussed RAII, I showed you classes that managed an object's lifetime and did something else (write to a file, allow memory access, control a lock).

Now that we have template tools available to us, let's try to write a templated RAII class that only manages lifetimes (we'll leave the functionality to the managed class)

Let's write an RAII wrapper class!

template <typename T>
class Manager {
   T* managed;

   Manager()     : managed(nullptr) { };
   Manager(T* t) : managed(t)       { }
   ~Manager()                       { delete managed; }
};
int main(){
  Dog* d = new Dog();
  Manager managed_dog(d);
  
  /* Really complicated logic */
  
  ...
  // Now we can't forget to free the memory
  // because it'll automatically be deleted when
  // managed_dog goes out of scope!
}
template <typename T>
class Manager {
   T* managed;

   Manager() = delete;
   Manager(T* t) : managed(t) { }
   ~Manager(){ delete managed; }
};
template <typename T>
void compute_manager(Manager<T> m){
  // Do something
}

int main(){
  Dog* d = new Dog();
  Manager managed_dog(d);
  compute_manager(m);
  
  //...oh dangnabbit
}
template <typename T>
class Manager {
   T* managed;

   Manager() = delete;
   Manager(T* t) : managed(t) { }
   ~Manager(){ delete managed; }
};
template <typename T>
void compute_manager(Manager<T> m){
  // Do something
}

int main(){
  Dog* d = new Dog();
  Manager managed_dog(d);
  compute_manager(m);
  do_other_stuff(m);
  //...oh dangnabbit
}

Copy is made here...

...and deleted here

Double free!

Use-after-free!

Many more issues!!

Having multiple managers who are...

  1. Managing the same entity

  2. Not talking to each other

This is true in pretty much any situation ever.

Fundamental Problem

...is a terrible idea.

How do we fix this?

Only one manager can ever exist for a managed object!

Let the managers talk to each other!

std::unique_ptr

std::shared_ptr

std::unique_ptr

std::unique_ptr

A built-in way of dynamically managing the lifetime of an object.

 

  • Wraps a pointer to a heap-allocated object
  • Only one unique_ptr can ever refer to a given heap object
  • Once the unique_ptr goes out of scope, the object is deleted.

How do we enforce the requirement that only one manager exists at once?

Enforcing Uniqueness

There are two ways that a unique pointer could become non-unique:

Dog* dog = new Dog()
std::unique_ptr<Dog> p1(dog);
std::unique_ptr<Dog> p2(p1);

By copying an existing unique_ptr

Dog* dog = new Dog()
std::unique_ptr<Dog> p1(dog);
std::unique_ptr<Dog> p2(dog);

By reusing a raw pointer

Eliminating Copies of unique_ptr

unique_ptr {
  T* ptr;
  ...
  unique_ptr(const unique_ptr& other) = delete;
  unique_ptr& operator=(const unique_ptr& other) = delete;

How do you make a class uncopyable?

  unique_ptr(unique_ptr&& other){
    std::swap(this->ptr, other.ptr)
  }
  unique_ptr& operator=(unique_ptr&& other){
    std::swap(this->ptr, other.ptr);
  }
  ~unique_ptr(){ delete this->ptr; }

Now this class can only be moved, not copied.

How do we solve the problem of someone reusing a pointer?

Dog* dog = new Dog()
std::unique_ptr<Dog> p1(dog);
std::unique_ptr<Dog> p2(dog);

Can't really ¯\_(ツ)_/¯

The solution would be to use rvalue references, but there are reasons why we can't do this.

std::make_unique

However, we can encourage the use of a factory function which disallows this issue!

struct X{
  X();
  X(int, int);
};
std::unique_ptr<X> xp = std::make_unique<X>(2, 3);

The arguments to make_unique are passed through to the constructor of X.

Example

struct Dog {
  Dog(std::string x);
};

void pet_dog(std::unique_ptr<Dog> d){
  // Blah blah blah
}

int main(){
  auto x = std::make_unique<Dog>("Spot");
  
  pet_dog(x); // Illegal, attemtpts to copy construct!
  
  pet_dog(std::move(x)); // Okay!
  
  pet_dog(make_unique<Dog>("Toby")); // Also okay!
}

What about the other solution?

Managers talk to each other?

std::shared_ptr

std::shared_ptr

A built-in way of dynamically managing the lifetime of an object.

 

  • Wraps a pointer to a heap-allocated object
  • Multiple shared_ptrs can exist to the same object.
  • Once the last shared_ptr goes out of scope, the object is deleted.

How do we enforce the requirement that the object can only be deleted when the last shared pointer is gone?

Anatomy of a shared_ptr

The simplest implementation of shared_ptr is a pair of pointers: one points to the data, and one points to a control block

shared_ptr

data

control block

1

What's in a control block?

Lots of things:

  • Pointer to data
  • Memory Allocator
  • Memory Deleter
  • Number of shared pointers referring to the object

1

Anatomy of a shared_ptr

When a new shared_ptr is copy constructed, increase the control block counter (first copy the control block pointer, then use it to increment counter)

shared_ptr

data

control block

1

shared_ptr

2

Anatomy of a shared_ptr

When a shared_ptr is deleted, decrement the control block counter in the destructor.

data

control block

1

shared_ptr

Anatomy of a shared_ptr

When the counter reaches zero, delete the control block and the data!

data

control block

0

shared_ptr should be copied!

The whole point of this class is that once there are no copies of the shared_ptr left, the managed memory cleans itself up!

 

Copy shared pointers around all you want!

 

Also, read the ISO C++ guidelines, which has lots of great advice on how to use smart pointers (see section "R").

std::make_shared

struct Dog {
  Dog(std::string x);
};

void pet_dog(std::shared_ptr<Dog> d){
  // Blah blah blah
}

int main(){
  auto x = std::make_shared<Dog>("Spot");
  
  pet_dog(x); // Okay! In fact, shared pointers should be copied!
  
  pet_dog(std::move(x)); // Also okay!
  
  pet_dog(make_shared<Dog>("Toby")); // Also okay!
}

Works just like std::make_unique

Reference Cycles

Cycles in shared_ptr can cause deletion to fail!

data

control block

1

shared_ptr

shared_ptr

Reference Cycles

Cycles in shared_ptr can cause deletion to fail!

data

control block

2

shared_ptr

shared_ptr

Reference Cycles

This can happen with any cycle, not just self-cycles!

data

control block

1

shared_ptr

Any cycle of shared_ptrs can cause memory leaks.

 

To fix this, use a weak_ptr somewhere in the cycle.

For those of you that have been asking about garbage collection in C++: the shared_ptr effectively implements reference-counting GC

The Forwarding Problem

template <typename T, typename U>
std::shared_ptr<T> make_shared(U&& arg){
  T* obj = new T(arg);
  return std::shared_ptr<T>(obj);
}

What does make_shared look like?

NB: this technically could result in violation of the "two shared pointers" rule, but since it's a very short function, it's relatively safe.

However, it breaks on very important functionality!

Trying to write factory functions like make_shared in the presence of the rvalue/lvalue distinction can be tricky!

class MoveOnly {
  MoveOnly() = default;
  MoveOnly(const MoveOnly&) = delete;
  MoveOnly& operator=(const MoveOnly&) = delete;
  MoveOnly(MoveOnly&&) = default;
  MoveOnly& operator=(MoveOnly&&) = delete;
};
int main(){
  MoveOnly a;
  make_shared<MoveOnly>(a);
  make_shared<MoveOnly>(MoveOnly());
}
template <typename T, typename U>
std::shared_ptr<T> make_shared(U&& arg){
  T* obj = new T(arg);
  return std::shared_ptr<T>(obj);
}

The first call is obviously illegal.

What about the second?

Let's say we can solve that problem

class MoveOrCopy {
  MoveOnly() = default;
  MoveOnly(const MoveOnly&) = default;
  MoveOnly& operator=(const MoveOnly&) = default;
  MoveOnly(MoveOnly&&) = default;
  MoveOnly& operator=(MoveOnly&&) = default;
};
int main(){
  MoveOnly a;
  make_shared<MoveOnly>(a);
  make_shared<MoveOnly>(MoveOnly());
}
template <typename T, typename U>
std::shared_ptr<T> make_shared(U&& arg){
  T* obj = new T(arg);
  return std::shared_ptr<T>(obj);
}

The first call is still illegal! The call is on an lvalue, and U&& is an rvalue reference.

template <typename T>
std::shared_ptr<T> make_shared(const Animal& in){
  T* obj = new T(in);
  return std::shared_ptr<T>(obj);
}

Solution: Forwarding References*

*Sometimes called "universal references"

When the form `T&&` appears in a type and T is subject to type inference (i.e. is `auto` or a template parameter), then T&& is not an rvalue reference.

 

It is a special type of reference called a forwarding reference that preserves the type it was initialized with:

  • If it is initialized from an rvalue, then it is an rvalue reference
  • If it is initialized from an lvalue, then it is an lvalue reference.
struct Animal {
  Animal(int& x){ std::cout << "Calling lvalue ref constructor\n";}
  Animal(int&& x){ std::cout << "Calling rvalue ref constructor\n";}
};

template <typename T, typename U>
std::shared_ptr<T> make_shared(U&& in){
  T* obj = new T(in);
  return std::shared_ptr<T>(obj);
}

Example: Forwarding References*

int main(){
  int i = 3;
  auto x = make_shared<Animal>(i);
}

/*
> ./a.out
> Calling lvalue ref constructor
*/
int main(){
  auto x = make_shared<Animal>(3);
}

/*
> ./a.out
> Calling lvalue ref constructor
*/

...wait, what?

Remember: rvalue references are lvalues!

We can preserve the lvalue/rvalue-ness of a forwarding reference by using std::forward

struct Animal {
  Animal(int& x){ std::cout << "Calling lvalue ref constructor\n";}
  Animal(int&& x){ std::cout << "Calling rvalue ref constructor\n";}
};

template <typename T, typename U>
std::shared_ptr<T> make_shared(U&& in){
  T* obj = new T(std::forward<U>(in));
  return std::shared_ptr<T>(obj);
}
int main(){
  int i = 3;
  auto x = make_shared<Animal>(i);
}

/*
> ./a.out
> Calling lvalue ref constructor
*/
int main(){
  auto x = make_shared<Animal>(3);
}

/*
> ./a.out
> Calling rvalue ref constructor
*/

At some point in project 3, if you haven't made some weird design decisions, you may find that some of your code does not work when passed rvalues, and that fixing it makes it not work when passed lvalues.

 

If that happens...look back over this section.

Summary

template <typename T>
class Manager {
   T* managed;

   Manager() = delete;
   Manager(T* t) : managed(t) { }
   ~Manager(){ delete managed; }
};
template <typename T>
void compute_manager(Manager<T> m){
  // Do something
}

int main(){
  Dog* d = new Dog();
  Manager managed_dog(d);
  compute_manager(m);
  
  //...oh dangnabbit
}

Having multiple non-communicating managers of an entity is almost always a bad idea!

We can manage objects (lifetimes) in C++ using two smart pointer classes, which solve the non-communicating manager problem .

Only one manager can ever exist for a managed object!

Let the managers talk to each other!

std::unique_ptr

std::shared_ptr

std::unique_ptr

unique_ptr solves the problem by only letting one unique_ptr own an object.

 

unique_ptr cannot be copied, only moved!

 

 

 

 

Use std::make_unique to create a unique_ptr without danger of double-owning a raw pointer.

unique_ptr {
  T* ptr;
  ...
  unique_ptr(const unique_ptr& other) = delete;
  unique_ptr& operator=(const unique_ptr& other) = delete;

std::shared_ptr

shared_ptr solves the problem by counting how many copies of the shared_ptr exist.

 

When the shared_ptr count hits zero, the object auto-destructs.

Reference cycles may prevent shared_ptrs from destructing, leaking memory.

 

Use std::make_shared to create a shared_ptr without danger of double-owning a raw pointer.

data

control block

1

Forwarding

There are two problems when trying to pass rvalue/lvalue references through intermediate functions to their appropriate constructors:

  • RValue references are lvalues
  • Sometimes we want to take both rvalues and lvalues

 

To solve this, we have a forwarding reference: a reference declared with T&& where T is type-deduced.

 

Forwarding references can be passed through a function while retaining their original "value category" (i.e. rvalue/lvalue-ness) with std::forward

The Rest of the Class

You now have all the information you need to complete the projects and

Notecards

  • Name and EID
  • One thing you learned today (can be "nothing")
  • One question you have about the material. If you leave this blank, you will be docked points.

    If you do not want your question to be put on Piazza, please write the letters NPZ and circle them.

Additional Resources