Kevin Song
I'm a student at UT (that's the one in Austin) who studies things.
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.
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.
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;
// 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);
}
};
ETC gen_etc();
int main(){
ETC e1 = gen_etc(); // Constructed by move
ETC e2 = e1; // Constructed by copy
}
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.
Smart Pointers and Perfect Forwarding
Episode 5: RAII Strikes Back!
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)
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!
This is true in pretty much any situation ever.
Only one manager can ever exist for a managed object!
Let the managers talk to each other!
A built-in way of dynamically managing the lifetime of an object.
How do we enforce the requirement that only one manager exists at once?
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
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.
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.
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.
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!
}
Managers talk to each other?
A built-in way of dynamically managing the lifetime of an object.
How do we enforce the requirement that the object can only be deleted when the last shared pointer is gone?
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
Lots of things:
1
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
When a shared_ptr is deleted, decrement the control block counter in the destructor.
data
control block
1
shared_ptr
When the counter reaches zero, delete the control block and the data!
data
control block
0
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").
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
Cycles in shared_ptr can cause deletion to fail!
data
control block
1
shared_ptr
shared_ptr
Cycles in shared_ptr can cause deletion to fail!
data
control block
2
shared_ptr
shared_ptr
This can happen with any cycle, not just self-cycles!
data
control block
1
shared_ptr
For those of you that have been asking about garbage collection in C++: the shared_ptr effectively implements reference-counting GC
template <typename T, typename U>
std::shared_ptr<T> make_shared(U&& arg){
T* obj = new T(arg);
return std::shared_ptr<T>(obj);
}
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!
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?
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);
}
*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:
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);
}
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?
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.
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
}
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!
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;
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
There are two problems when trying to pass rvalue/lvalue references through intermediate functions to their appropriate constructors:
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
You now have all the information you need to complete the projects and
By Kevin Song
Perfect Forwarding and Smart Pointers
I'm a student at UT (that's the one in Austin) who studies things.