CSE333(15-16): C++ STL and Smart Pointers
Lecture 15 - C++ Standard Template Library
four major pieces
- The entire C standard library
- C++’s input/output stream library
- std::cin, std::cout, stringstreams, fstreams, etc.
- C++’s standard template library (STL) +Containers, iterators, algorithms (sort, find, etc.), numerics
- C++’s miscellaneous library
- Strings, exceptions, memory allocation, localization
STL Containers
A container is an object that stores (in memory) a collection of other objects (elements).
Several different classes of container:
- Sequence containers (vector, deque, list, …)
- Associative containers (set, map, multiset, multimap, bitset, …)
- Differ in algorithmic cost and supported operations
STL containers store by value
, not by reference:
- When you insert an object, the container makes a copy
- If the container needs to rearrange objects, it makes copies
- e.g. if you sort a vector, it will make many, many copies
- e.g. if you insert into a map, that may trigger several copies
- What if you don’t want this (disabled copy constructor or copying
is expensive)?
- You can insert a wrapper object with a pointer to the object (use smart pointer)
STL vector
Vector, A generic, dynamically resizable array.
- Elements are stored in contiguous memory locations
- Elements can be accessed using pointer arithmetic if you’d like
- Random access is O(1) time
- Adding/removing from the end is cheap (amortized constant time)
- Inserting/deleting from the middle or start is expensive (linear time)
Each container class has an associated iterator class (e.g. vector::iterator) used to iterate through elements of the container.
Lecture 16 - C++ Smart Pointers
Intro and toy_ptr
We noticed that STL was doing an enormous amount of copying
- solution: store pointers in containers instead of objects
But who’s responsible for deleting and when???
A smart pointer is an object that stores a pointer to a heap-allocated object
- A smart pointer looks and behaves like a regular C++ pointer
- By overloading * , ->, [], etc.
- These can help you manage memory
- The smart pointer will delete the pointed-to object at the right time including invoking the object’s destructor, When that is depends on what kind of smart pointer you use
- With correct use of smart pointers, you no longer have to remember when to delete new’d memory!
A Toy Smart Pointer
We can implement a simple one with:
- A constructor that accepts a pointer
- A destructor that frees the pointer
- Overloaded * and -> operators that access the pointer
ToyPtr.cc
#ifndef _TOYPTR_H_
#define _TOYPTR_H_
template <typename T> class ToyPtr {
public:
ToyPtr(T *ptr) : ptr_(ptr) { } // constructor
~ToyPtr() { // destructor
if (ptr_ != nullptr) {
delete ptr_;
ptr_ = nullptr;
}
}
T &operator*() { return *ptr_; } // * operator
T *operator->() { return ptr_; } // -> operator
private:
T *ptr_; // the pointer itself
};
#endif // _TOYPTR_H_
usetoy.cc:
#include <iostream>
#include "ToyPtr.h"
// simply struct to use
typedef struct { int x = 1, y = 2; } Point;
std::ostream &operator<<(std::ostream &out, const Point &rhs) {
return out << "(" << rhs.x << "," << rhs.y << ")";
}
int main(int argc, char **argv) {
// Create a dumb pointer
Point *leak = new Point;
// Create a "smart" pointer (OK, it's still pretty dumb)
ToyPtr<Point> notleak(new Point);
std::cout << " *leak: " << *leak << std::endl;
std::cout << " leak->x: " << leak->x << std::endl;
std::cout << " *notleak: " << *notleak << std::endl;
std::cout << "notleak->x: " << notleak->x << std::endl;
return 0;
}
std::unique_ptr
A unique_ptr
takes ownership of a pointer:
- A template: template parameter is the type that the “owned” pointer references (i.e., the T in pointer type T*)
- Part of C++’s standard library (C++11)
- Its destructor invokes
delete
on the owned pointer- Invoked when
unique_ptr
object isdelete
’d or falls out of scope
- Invoked when
unique1.cc:
#include <iostream> // for std::cout, std::endl
#include <memory> // for std::unique_ptr
#include <cstdlib> // for EXIT_SUCCESS
void Leaky() {
int *x = new int(5); // heap-allocated
(*x)++;
std::cout << *x << std::endl;
} // never use delete, therefore leak
void NotLeaky() {
std::unique_ptr<int> x(new int(5)); // wrapped, heap-allocated
(*x)++;
std::cout << *x << std::endl;
} // never used delete, but no leak
int main(int argc, char **argv) {
Leaky();
NotLeaky();
return EXIT_SUCCESS;
}
If you have many potential exits out of a function, it’s easy to forget to call delete on all of them
unique_ptr
will delete its pointer when it falls out of scope- Thus, a unique_ptr also helps with exception safety
unique2.cc:
#include <memory> // for std::unique_ptr
#include <cstdlib> // for EXIT_SUCCESS
using namespace std;
typedef struct { int a, b; } IntPair;
int main(int argc, char **argv) {
unique_ptr<int> x(new int(5));
int *ptr = x.get(); // Return a pointer to pointed-to object
int val = *x; // Return the value of pointed-to object
// Access a field or function of a pointed-to object
unique_ptr<IntPair> ip(new IntPair);
ip->a = 100;
// Deallocate current pointed-to object and store new pointer
x.reset(new int(1));
ptr = x.release(); // Release responsibility for freeing
delete ptr;
return EXIT_SUCCESS;
}
Transferring Ownership
Use reset()
and release()
to transfer ownership:
- release returns the pointer, sets wrapped pointer to nullptr
- reset delete’s the current pointer and stores a new one
int main(int argc, char **argv) {
unique_ptr<int> x(new int(5));
cout << "x: " << x.get() << endl;
unique_ptr<int> y(x.release()); // x abdicates ownership to y
cout << "x: " << x.get() << endl;
cout << "y: " << y.get() << endl;
unique_ptr<int> z(new int(10));
// y transfers ownership of its pointer to z.
// z's old pointer was delete'd in the process.
z.reset(y.release());
return EXIT_SUCCESS;
}
unique_ptrs Cannot Be Copied
std::unique_ptr
has disabled its copy constructor and assignment operator- You cannot copy a unique_ptr, helping maintain “uniqueness” or “ownership”
#include <memory> // for std::unique_ptr
#include <cstdlib> // for EXIT_SUCCESS
int main(int argc, char **argv) {
std::unique_ptr<int> x(new int(5)); // OK
std::unique_ptr<int> y(x); // fail – no copy ctor
std::unique_ptr<int> z; // OK – z is nullptr
z = x; // fail – no assignment op
return EXIT_SUCCESS;
}
Copy Semantics
Assigning values typically means making a copy
- Sometimes this is what you want
- e.g. assigning a string to another makes a copy of its value
- Sometimes this is wasteful
- e.g. assigning a returned string goes through a temporary copy
std::string ReturnFoo(void) {
std::string x("foo");
return x; // this return might copy
}
int main(int argc, char **argv) {
std::string a("hello");
std::string b(a); // copy a into b
b = ReturnFoo(); // copy return value into b
return EXIT_SUCCESS;
}
Move Semantics
Move semantics
move values from one object to another without copying (“stealing”)
- Useful for optimizing away temporary copies
- A complex topic that uses things called “rvalue references”
std::string ReturnFoo(void) {
std::string x("foo");
// this return might copy
return x;
}
int main(int argc, char **argv) {
std::string a("hello");
// moves a to b
std::string b = std::move(a);
std::cout << "a: " << a << std::endl;
std::cout << "b: " << b << std::endl;
// moves the returned value into b
b = std::move(ReturnFoo());
std::cout << "b: " << b << std::endl;
return EXIT_SUCCESS;
}
unique_ptr and STL Containers
unique_ptrs
can be stored in STL containers by using move semantics.
Transferring Ownership via Move:
- Can move ownership from one
unique_ptr
to another - Behavior is equivalent to the ‘release-and-reset’ combination
unique_ptr<int> y = std::move(x);
, x release, y reset?- Smart pointer can’t be copied…
unique_ptr and Vector
int main(int argc, char **argv) {
std::vector<std::unique_ptr<int> > vec; //a vector array of smart pointers
vec.push_back(std::unique_ptr<int>(new int(9))); // push back a smart pointer
vec.push_back(std::unique_ptr<int>(new int(5)));
vec.push_back(std::unique_ptr<int>(new int(7)));
// z gets a copy of int value pointed to by vec[1]
int z = *vec[1];
std::cout << "z is: " << z << std::endl;
// won’t compile! Cannot copy unique_ptr
std::unique_ptr<int> copied = vec[1]; // hmmm... failed
// Works! vec[1] after the statement below will wrap a nullptr
std::unique_ptr<int> moved = std::move(vec[1]);
std::cout << "*moved: " << *moved << std::endl;
std::cout << "vec[1].get(): " << vec[1].get() << std::endl;
return EXIT_SUCCESS;
}
unique_ptr and ‘<’
A unique_ptr implements some comparison operators, including operator <
- However, it doesn’t invoke operator
<
on the pointed-to objects. - Instead, it just promises a stable, strict ordering (probably based on the pointer address, not the pointed-to-value)
- So to use
sort()
on vectors, you want to provide it with a comparison function
using namespace std;
bool sortfunction(const unique_ptr<int> &x,
const unique_ptr<int> &y) { return *x < *y; }
void printfunction(unique_ptr<int> &x) { cout << *x << endl; }
int main(int argc, char **argv) {
vector<unique_ptr<int> > vec;
vec.push_back(unique_ptr<int>(new int(9)));
vec.push_back(unique_ptr<int>(new int(5)));
vec.push_back(unique_ptr<int>(new int(7)));
// buggy: sorts based on the values of the ptrs
sort(vec.begin(), vec.end());
cout << "Sorted:" << endl;
for_each(vec.begin(), vec.end(), &printfunction);
// better: sorts based on the pointed-to values
sort(vec.begin(), vec.end(), &sortfunction);
cout << "Sorted:" << endl;
for_each(vec.begin(), vec.end(), &printfunction);
return EXIT_SUCCESS;
}
unique_ptr, “<”, and maps
- Similarly, you can use unique_ptrs as keys in a map
- A map internally stores keys in a sorted order, iterating through the map iterates through the keys in order
- By default, “<” is used to enforce ordering
- You must specify a comparator when constructing the map to get a meaningful sorted order using “<” of unique_ptrs
Example:
struct MapComp {
bool operator()(const unique_ptr<int> &lhs,
const unique_ptr<int> &rhs) const { return *lhs < *rhs; }
};
int main(int argc, char **argv) {
map<unique_ptr<int>, int, MapComp> a_map; // Create the map
unique_ptr<int> a(new int(5)); // unique_ptr for key
unique_ptr<int> b(new int(9));
unique_ptr<int> c(new int(7));
a_map[std::move(a)] = 25; // move semantics to get ownership
a_map[std::move(b)] = 81; // of unique_ptrs into the map.
a_map[std::move(c)] = 49; // a, b, c hold NULL after this.
map<unique_ptr<int>,int>::iterator it;
for (it = a_map.begin(); it != a_map.end(); it++) {
std::cout << "key: " << *(it->first);
std::cout << " value: " << it->second << std::endl;
}
return EXIT_SUCCESS;
}
unique_ptr and Arrays
unique_ptr can store arrays as well, will call delete[] on destruction.
#include <memory> // for std::unique_ptr
#include <cstdlib> // for EXIT_SUCCESS
using namespace std;
int main(int argc, char **argv) {
unique_ptr<int[]> x(new int[5]);
x[0] = 1;
x[2] = 2;
return EXIT_SUCCESS;
}
Reference counting
Idea: associate a reference count with each object
- Reference count holds number of references (pointers) to the object
- Adjusted whenever pointers are changed:
- Increase by 1 each time we have a new pointer to an object
- Decrease by 1 each time a pointer to an object is removed
- When reference counter decreased to 0, no more pointers to the object, so delete it (automatically)
- Used by C++
shared_ptr
, not used in general for C++ memory management
Noted that:
- Bunch of extra overhead on every pointer operation
- Cannot reclaim linked objects with circular references
- Not general enough for automatic memory management (need automatic garbage collection as in Java), but when it’s appropriate it’s a clean solution for resource management and cleanup
- directory links to files in Linux – delete file when link count = 0!
std::shared_ptr and std::weak_ptr
std::shared_ptr
shared_ptr is similar to unique_ptr but we allow shared objects to have multiple owners:
- The copy/assign operators are not disabled and increment or
decrement
reference counts
as needed- After a copy/assign, the two shared_ptr objects point to the same pointed-to object and the (shared) reference count is 2
- When a shared_ptr is destroyed, the reference count is decremented
- When the reference count hits 0, we
delete
the pointed-to object!
- When the reference count hits 0, we
- Allows us to create complex linked structures (double-linked lists, graphs, etc.) at the cost of maintaining reference counts
- A loop(cycle) of linked list may cause memory leak!
Memory Leak Example:
Node * q = new Node();
Node * r = new Node();
q->next = r;
r->next = q;
r = nullptr;
q = nullptr;
Shared Pointer Example:
#include <cstdlib> // for EXIT_SUCCESS
#include <iostream> // for std::cout, std::endl
#include <memory> // for std::shared_ptr
int main(int argc, char **argv) {
std::shared_ptr<int> x(new int(10)); // ref count: 1
// temporary inner scope with local y (!)
{
std::shared_ptr<int> y = x; // ref count: 2
std::cout << *y << std::endl;
} // exit scope, y deleted
std::cout << *x << std::endl; // ref count: 1
return EXIT_SUCCESS;
} // ref count: 0
std::shared_ptr and STL Containers
Even simpler than unique_ptrs
, Safe to store shared_ptrs
in containers, since copy & assign maintain a shared reference count.
vector<std::shared_ptr<int> > vec;
vec.push_back(std::shared_ptr<int>(new int(9)));
vec.push_back(std::shared_ptr<int>(new int(5)));
vec.push_back(std::shared_ptr<int>(new int(7)));
int &z = *vec[1];
std::cout << "z is: " << z << std::endl;
std::shared_ptr<int> copied = vec[1]; // works!
std::cout << "*copied: " << *copied << std::endl;
std::shared_ptr<int> moved = std::move(vec[1]); // works!
std::cout << "*moved: " << *moved << std::endl;
std::cout << "vec[1].get(): " << vec[1].get() << std::endl;
std::weak_ptr
To solve the Cycle of shared_ptrs problem.
weak_ptr
is similar to a shared_ptr but doesn’t affect the reference count
- Can only “point to” an object that is managed by a shared_ptr
- Not really a pointer, we can’t actually dereference using
*
unless you “get” its associated shared_ptr - Because it doesn’t influence the reference count, weak_ptrs can become “dangling”
- Object referenced may have been delete’d
- But you can check to see if the object still exists
Breaking the Cycle with weak_ptr
:
#include <cstdlib>
#include <memory>
using std::shared_ptr;
using std::weak_ptr;
struct A {
shared_ptr<A> next;
weak_ptr<A> prev;
};
int main(int argc, char **argv) {
shared_ptr<A> head(new A());
head->next = shared_ptr<A>(new A());
head->next->prev = head;
return EXIT_SUCCESS;
}
Now what happens when we delete head? Ref counts go to 0 and nodes deleted!
#include <cstdlib> // for EXIT_SUCCESS
#include <iostream> // for std::cout, std::endl
#include <memory> // for std::shared_ptr, std::weak_ptr
int main(int argc, char **argv) {
std::weak_ptr<int> w;
{ // temporary inner scope with local x
std::shared_ptr<int> x;
{ // temporary inner-inner scope with local y
std::shared_ptr<int> y(new int(10));
w = y; // weak ref; ref count for “10” node is same
x = w.lock(); // get "promoted" shared_ptr, ref cnt = 2
std::cout << *x << std::endl;
} // y deleted; ref count now 1
std::cout << *x << std::endl;
} // x deleted; ref count now 0; mem freed
std::shared_ptr<int> a = w.lock(); // nullptr
std::cout << a << std::endl; // output is 0 (null)
return EXIT_SUCCESS;
}
Summary
- A unique_ptr takes ownership of a pointer
- Cannot be copied, but can be moved
- get() returns a copy of the pointer, but is dangerous to use; better to use release() instead
- reset() deletes old pointer value and stores a new one
- A shared_ptr allows shared objects to have multiple owners by doing reference counting
- deletes an object once its reference count reaches zero
- A weak_ptr works with a shared object but doesn’t affect the reference count
- Can’t actually be dereferenced, but can check if the object still exists and can get a shared_ptr from the weak_ptr if it does
Hope that I can be as smart as the smart pointer…