COMP6771
Advanced C++ Programming
Week 5.2
Smart Pointers
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
-
auto_ptr;Unique pointer, shared pointer, Weak - 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
- auto_ptr vs unique_ptr
- manage the lifetime of its resources
- allocate/deallocate according to RAII (release resourse)
- support automatic memory management
- 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
- use std::unique_prt for exclusive ownership resource management.
- 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 that handles DMA in restricted scope h
- When the unique pointer is destructed, the underlying object is too
- Can be parameterized with deleter:std::unique_pointer<T, deleter>
- No additional/very tiny overhead compared to raw
-
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
void my_func()
{
int* valuePtr = new int(15);
int x = 45;
// ...
if (x == 45)
return; // here we have a memory leak, valuePtr is not deleted
// ...
delete valuePtr;
}
int main()
{
}
#include <memory>
void my_func()
{
std::unique_ptr<int> valuePtr(new int(15));
int x = 45;
// ...
if (x == 45)
return; // no memory leak anymore!
// ...
}
int main()
{
}
std::unique_ptr<int> valuePtr(new int(47));
std::unique_ptr<int> valuePtr;
valuePtr.reset(new int(47));
//can be accessed just like when you would use a raw pointer:
std::unique_ptr<std::string> strPtr(new std::string);
strPtr->assign("Hello world");
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
#include <iostream>
#include <memory>
#include <utility>
int main()
{
std::unique_ptr<int> valuePtr(new int(15));
std::unique_ptr<int> valuePtrNow(std::move(valuePtr));
std::cout << "valuePtrNow = " << *valuePtrNow << '\n';
std::cout << "Has valuePtr an associated object? "
<< std::boolalpha
<< static_cast<bool>(valuePtr) << '\n';
}
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.
make_unique
is safe for creating temporaries, whereas with explicit use of new you have to remember the rule about not using unnamed temporaries.
make_unique prevents the unspecified-evaluation-order leak triggered by expressions like
demo554-unique2.cpp
foo(unique_ptr<T>(new T()), unique_ptr<U>(new U())); // unsafe*
foo(make_unique<T>(), make_unique<U>()); // exception safe // however no impact on efficiecy
Text
Text
Text
Text
Unique_ptr Array
auto pArr = std::unique_ptr<MyClass[]>(new MyClass[10]);
can be specialized for array std::unique_pointer<T []>
unique_ptr disposes of the controlled object by calling deleter .what what about unique_ptr to array of objects?
#include <iostream>
#include <memory>
int main()
{
const int size = 10;
std::unique_ptr<int[]> fact(new int[size]);
for (int i = 0; i < size; ++i) {
fact[i] = (i == 0) ? 1 : i * fact[i-1];
}
for (int i = 0; i < size; ++i) {
std::cout << i << "! = " << fact[i] << '\n';
}
}
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_ptr
, unlike unique_ptr
, places a layer of indirection between the physical heap-allocated object and the notion of ownership.
shared_ptr
instances are essentially participating in ref-counted ownership of the control block.
The control block itself is the sole arbiter of what it means to “delete the controlled object.”
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); //no ownership
//ref to objected managed by shared pointer
auto wp = std::weak_ptr<int>(x); // x owns the memory
//wp.use_count(); wp.expired();
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
struct Person;
struct Team{
shared_ptr<Person> goalKeeper;
~Team(){cout<<"Team destructed.";}
};
struct Person{
shared_ptr<Team> team;
~Person(){cout<<"Person destructed.";}
};
int main(){
auto Barca = make_shared<Team>();
auto Valdes = make_shared<Person>();
Barca->goalKeeper = Valdes;
Valdes->team = Barca;
return 0;
}
struct Person;
struct Team{
shared_ptr<Person> goalKeeper;
~Team(){cout<<"Team destructed.";}
};
struct Person{
weak_ptr<Team> team; // This line is changed.
~Person(){cout<<"Person destructed.";}
};
int main(){
auto Barca = make_shared<Team>();
auto Valdes = make_shared<Person>();
Barca->goalKeeper = Valdes;
Valdes->team = Barca;
return 0;
}
If Barca goes out of scope, it is not deleted since the managed object is still pointed by valdee.team. When Valdes goes out of scope, its managed object is not deleted either as it is pointed by Barca.goalkeeper.
Text
Own no resourse: orrows from shared ptr
break circular dependency
We have to convert it into Shared_ptr to use it
wp.lock();
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)
Use Smart Pointers Efficiently but still use raw pointer and references ? they are not bad
Best practice: smart pointers, and minimize raw pointers or say big NO to raw
Raw pointer should be your default parameters and return types
sometime trade-off smart vs raw
- argument passing; but references can't be null, so are preferable
- A points to B, B points to A, or A->B->C->A
raw vs smart --->Premature Pessimizzation
if an entity must take a certain kind of ownership of the object, always use smart pointers - the one that gives you the kind of ownership you need.
If there is no notion of ownership, you may ignore use smart pointers but.
void PrintObject(shared_ptr<const Object> po) //bad
{
if(po)
po->Print();
else
log_error();
}
void PrintObject(const Object* po) //good
{
if(po)
po->Print();
else
log_error();
}
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};
auto uni = std::unique_ptr<int>(ptr);
g();
}
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
- comparatively rare in the wild
- challenge for language designers wanting to provide guarantees around invariants, immutability and concurrency-safety, and non-nullability.
-
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 i.e. root/derived
- Except for an exception thrown in a constructor that delegates (why?)
-
two common
- ‘this’ is leaked out of a constructor to some code that assumes the object has been initialized. [dont do that]
- A failure partway through an object’s construction leads to its destructor or finalizer running against a partially-constructed object. [tread with care]
#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
- Safe approach: dont make it available until constructed fully
-
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
make_shared and make_unique
-
make_shared
andmake_unique
wrap rawnew
, just as~shared_ptr
and~unique_ptr
wrap rawdelete
. -
Never touch raw pointers with hands, and then never need to worry about leaking them.
-
make_shared
can be performance optimization.
Feedback
COMP6771 22T2 - 5.2 - Smart Pointers
By imranrazzak
COMP6771 22T2 - 5.2 - Smart Pointers
- 393