COMP6771
Advanced C++ Programming
Week 5.2
Smart Pointers
Author: Hayden Smith
In this lecture
Why?
- Managing unnamed / heap memory can be dangerous, as there is always the chance that the resource is not released / free'd properly. We need solutions to help with this.
What?
- Smart pointers
- Unique pointer, shared pointer
- Partial construction
Recap: RAII - Making unnamed objects safe
// myintpointer.h
class MyIntPointer {
public:
// This is the constructor
MyIntPointer(int* value);
// This is the destructor
~MyIntPointer();
int* GetValue();
private:
int* value_;
};
// myintpointer.cpp
#include "myintpointer.h"
MyIntPointer::MyIntPointer(int* value): value_{value} {}
int* MyIntPointer::GetValue() {
return value_
}
MyIntPointer::~MyIntPointer() {
// Similar to C's free function.
delete value_;
}
Don't use the new / delete keyword in your own code
We are showing for demonstration purposes
void fn() {
// Similar to C's malloc
MyIntPointer p{new int{5}};
// Copy the pointer;
MyIntPointer q{p.GetValue()};
// p and q are both now destructed.
// What happens?
}
demo551-safepointer.cpp
Smart Pointers
- Ways of wrapping unnamed (i.e. raw pointer) heap objects in named stack objects so that object lifetimes can be managed much easier
- Introduced in C++11
- Usually two ways of approaching problems:
- unique_ptr + raw pointers ("observers")
- shared_ptr + weak_ptr/raw pointers
Type | Shared ownership | Take ownership |
---|---|---|
std::unique_ptr<T> | No | Yes |
raw pointers | No | No |
std::shared_ptr<T> | Yes | Yes |
std::weak_ptr<T> | No | No |
Unique pointer
-
std::unique_pointer<T>
- The unique pointer owns the object
- When the unique pointer is destructed, the underlying object is too
-
raw pointer (observer)
- Unique Ptr may have many observers
- This is an appropriate use of raw pointers (or references) in C++
- Once the original pointer is destructed, you must ensure you don't access the raw pointers (no checks exist)
- These observers do not have ownership of the pointer
Also note the use of 'nullptr' in C++ instead of NULL
Unique pointer: Usage
#include <memory>
#include <iostream>
int main() {
auto up1 = std::unique_ptr<int>{new int};
auto up2 = up1; // no copy constructor
std::unique_ptr<int> up3;
up3 = up2; // no copy assignment
up3.reset(up1.release()); // OK
auto up4 = std::move(up3); // OK
std::cout << up4.get() << "\n";
std::cout << *up4 << "\n";
std::cout << *up1 << "\n";
}
demo552-unique1.cpp
Observer Ptr: Usage
#include <memory>
#include <iostream>
int main() {
auto up1 = std::unique_ptr<int>{new int{0}};
*up1 = 5;
std::cout << *up1 << "\n";
auto op1 = up1.get();
*op1 = 6;
std::cout << *op1 << "\n";
up1.reset();
std::cout << *op1 << "\n";
}
Can we remove "new" completely?
demo553-observer.cpp
Unique Ptr Operators
#include <iostream>
#include <memory>
auto main() -> int {
// 1 - Worst - you can accidentally own the resource multiple
// times, or easily forget to own it.
// auto* silly_string = new std::string{"Hi"};
// auto up1 = std::unique_ptr<std::string>(silly_string);
// auto up11 = std::unique_ptr<std::string>(silly_string);
// 2 - Not good - requires actual thinking about whether there's a leak.
auto up2 = std::unique_ptr<std::string>(new std::string("Hello"));
// 3 - Good - no thinking required.
auto up3 = std::make_unique<std::string>("Hello");
std::cout << *up2 << "\n";
std::cout << *up3 << "\n";
// std::cout << *(up3.get()) << "\n";
// std::cout << up3->size();
}
This method avoids the need for "new". It has other benefits that we will explore.
demo554-unique2.cpp
Shared pointer
- std::shared_pointer<T>
-
Several shared pointers share ownership of the object
- A reference counted pointer
- When a shared pointer is destructed, if it is the only shared pointer left pointing at the object, then the object is destroyed
-
May also have many observers
- Just because the pointer has shared ownership doesn't mean the observers should get ownership too - don't mindlessly copy it
-
std::weak_ptr<T>
-
Weak pointers are used with share pointers when:
- You don't want to add to the reference count
- You want to be able to check if the underlying data is still valid before using it.
-
Weak pointers are used with share pointers when:
Shared pointer: Usage
#include <iostream>
#include <memory>
auto main() -> int {
auto x = std::make_shared<int>(5);
std::cout << "use count: " << x.use_count() << "\n";
std::cout << "value: " << *x << "\n";
x.reset(); // Memory still exists, due to y.
std::cout << "use count: " << y.use_count() << "\n";
std::cout << "value: " << *y << "\n";
y.reset(); // Deletes the memory, since
// no one else owns the memory
std::cout << "use count: " << x.use_count() << "\n";
std::cout << "value: " << *y << "\n";
}
Can we remove "new" completely?
demo555-shared.cpp
Weak Pointer: Usage
#include <iostream>
#include <memory>
auto main() -> int {
auto x = std::make_shared<int>(1);
auto wp = std::weak_ptr<int>(x); // x owns the memory
auto y = wp.lock();
if (y != nullptr) { // x and y own the memory
// Do something with y
std::cout << "Attempt 1: " << *y << '\n';
}
}
demo556-weak.cpp
When to use which type
-
Unique pointer vs shared pointer
- You almost always want a unique pointer over a shared pointer
-
Use a shared pointer if either:
- An object has multiple owners, and you don't know which one will stay around the longest
- You need temporary ownership (outside scope of this course)
- This is very rare
Smart pointer examples
- Linked list
- Doubly linked list
- Tree
- DAG (mutable and non-mutable)
- Graph (mutable and non-mutable)
- Twitter feed with multiple sections (eg. my posts, popular posts)
Stack unwinding
- Stack unwinding is the process of exiting the stack frames until we find an exception handler for the function
-
This calls any destructors on the way out
- Any resources not managed by destructors won't get freed up
- If an exception is thrown during stack unwinding, std::terminate is called
void g() {
throw std::runtime_error{""};
}
int main() {
auto ptr = new int{5};
g();
// Never executed.
delete ptr;
}
void g() {
throw std::runtime_error{""};
}
int main() {
auto ptr = std::make_unique<int>(5);
g();
}
Not safe
Safe
void g() {
throw std::runtime_error{""};
}
int main() {
auto ptr = new int{5};
g();
auto uni = std::unique_ptr<int>(ptr);
}
Not safe
Exceptions & Destructors
- During stack unwinding, std::terminate() will be called if an exception leaves a destructor
- The resources may not be released properly if an exception leaves a destructor
- All exceptions that occur inside a destructor should be handled inside the destructor
-
Destructors usually don't throw, and need to explicitly opt in to throwing
- STL types don't do that
Partial construction
-
What happens if an exception is thrown halfway through a constructor?
- The C++ standard: "An object that is partially constructed or partially destroyed will have destructors executed for all of its fully constructed subobjects"
- A destructor is not called for an object that was partially constructed
- Except for an exception thrown in a constructor that delegates (why?)
#include <exception>
class my_int {
public:
my_int(int const i) : i_{i} {
if (i == 2) {
throw std::exception();
}
}
private:
int i_;
};
class unsafe_class {
public:
unsafe_class(int a, int b)
: a_{new my_int{a}}
, b_{new my_int{b}}
{}
~unsafe_class() {
delete a_;
delete b_;
}
private:
my_int* a_;
my_int* b_;
};
int main() {
auto a = unsafe_class(1, 2);
}
Spot the bug
demo557-bad.cpp
Partial construction: Solution
-
Option 1: Try / catch in the constructor
- Very messy, but works (if you get it right...)
- Doesn't work with initialiser lists (needs to be in the body)
-
Option 2:
-
An object managing a resource should initialise the resource last
- The resource is only initialised when the whole object is
- Consequence: An object can only manage one resource
- If you want to manage multiple resources, instead manage several wrappers , which each manage one resource
-
An object managing a resource should initialise the resource last
#include <exception>
#include <memory>
class my_int {
public:
my_int(int const i)
: i_{i} {
if (i == 2) {
throw std::exception();
}
}
private:
int i_;
};
class safe_class {
public:
safe_class(int a, int b)
: a_(std::make_unique<my_int>(a))
, b_(std::make_unique<my_int>(b))
{}
private:
std::unique_ptr<my_int> a_;
std::unique_ptr<my_int> b_;
};
int main() {
auto a = safe_class(1, 2);
}
demo558-partial1.cpp
Feedback
COMP6771 21T2 - 5.2 - Smart Pointers
By haydensmith
COMP6771 21T2 - 5.2 - Smart Pointers
- 757