Dynamic Memory

Table of contents

  1. Overview
  2. Shared_ptr
  3. Manage memory directly
  4. Unique_ptr
  5. weak_ptr
  6. Dynamic arrays
  7. Allocator

1. Overview ↑top

Static memory is used for local static objects, for class static data members and for variables defined outside any func. Stack memory is used for nonstatic objects defined inside funcs. Objs allocated in static or stack memory are automatically created and destroyed by the compiler.

In addition to static or stack memory, programs can use heap memory to dynamically allocate objects. Dynamic memory is managed through a pair of operators:

To make using dynamic memory easier (and safer), the new library provides two smart pointer types that manage dynamic objs:

2. shared_ptr ↑top

shared_ptr is a smart ptr that retains shared ownership of an obj through a ptr. Several shared_ptr objs may own the same obj. The obj is destroyed and its mem deallocated when either of the following happens:

The obj is destroyed using delete-expression or a custom deleter that is supplied to shared_ptr during construction.

//a default inited smart ptr holds a null ptr
shared_ptr<string> p1;      //point at a str
shared_ptr<list<int>> p2;   //point at a list of ints

//smart ptr is used similarly to normal ptr
//if p1 is not null, check whether it's the empty str
if(p1 && p1->empty())
    *p1 = "hi"; //if so, deref p1 to assign a new val to that str

(i) make_shared function

make_shared allocates and initializes an obj in dynamic memory, and returns a shared_ptr that points to that obj. Like the sequential-container emplace members, make_shared uses its arguments to construct an obj of the given type.

//shared_ptr that points to an int with val 42
shared_ptr<int> p3 = make_shared<int>(42);
//p4 points to a string with value 99999
shared_ptr<string> p4 = make_shared<string>(5, '9');
//p5 points to an int that is value inited to 0
shared_ptr<int> p5 = make_shared<int>();
//p6 points to a dynamically allocated, empty vector<string>
auto p6 = make_shared<vector<string>>();

(ii) copying and assigning shared_ptrs

when we copy or assign a shared_ptr, each shared_ptr keeps track of how many other shared_ptrs point to the same obj:

auto p = make_shared<int>(42);  //obj to which p points has one user
auto q(p);                      //p and q points to the same obj
                                //obj to which p and q point has two users

shared_ptr can be thought to have an associated counter, called as reference count. Whenever we copy a shared_ptr, the count is incremented. E.g., the counter is incremented when we use it to init another shared_ptr, when we use it as the right-hand operand of an assignment, or when we pass it to or return it from a func by value; the counter is decremented when we assign a new value to the shared_ptr and when the shared_ptr itself is destroyed, such as when a local shared_ptr goes out of scope.

Once a shared_ptr's counter goes to zero, the shared_ptr automatically frees the obj that it manges:

auto r = make_shared<int>(42);  //int to which r points has one user
r = q;      //assign to r, making it point to different addr
            //increase the use count for the obj to which q points
            //reduce the use count of the obj to which r had pointed
            //the obj r had pointed to has no users; that obj is automatically freed

shared_ptrs automatically destroy their objects, and automatically free the associated memory:
The dstr for shared_ptr decrements the ref count of the obj to which that shared_ptr points. If the count goes to zero, the shared_ptr dstr destroys the obj to which the shared_ptr points and frees the mem used by that obj (calling the obj's dstr).

void use_factory(T arg) {
    shared_ptr<Foo> p = factory(arg);
    //use p
} //p goes out scope; the mem to which p points is auto freed

shared_ptr<Foo> use_factory(T arg) {
    shared_ptr<Foo> p = factory(arg);
    //use p
    return p;   //ref count is incremented when we rtn p
} //p goes out of scope; the mem to which p points is not freed

(iii) dynamic lifetime

Programs tend to use dynamic memory for one of three purposes:

vector<string> v1;      //empty vector
{ //new scope
    vector<string> v2 = {"a", "an", "the"};
    v1 = v2;            //copies the elems from v2 into v1
} //v2 is destroyed, which destroys the elems in v2
  //v1 has three elems, which are copies of the ones orginally in v2
  
Blob<string> b1;        //emoty Blob
{ //new scope
    Blob<string> b2 = {"a", "an", "the"};
    b1 = b2;            //b1 and b2 share the same elems
} //b2 is destroyed, but the elems in b2 must not be destroyed
  //b1 points to the elems originally created in b2

3. Manage memory directly ↑top

(i) new

new constructs an obj of type int on heap and returns a pointer to that object. By default, dynamically allocated objects are default inited:

int *pi = new int;          //pi points to dynamically allocated
                            //unamed, uninitialized int
                        
string *ps = new string;    //inited to empty string
int *pi = new int(1024);    //obj to which pi points has value 1024
vector<int> *pv = new vector<int>{0,1,2,3,4};

//allocate and init a const int
const int *pci = new const int(1024);
//allocate a default-inited const empty string
const string *pcs = new const string;

(ii) free

free destroys the obj to which its given ptr points, and it frees the crspding memory.

int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;

delete i;       //ERROR: i is not a pointer
delete pi1;     //undefined: pi1 refers to a local
                //, which is a statically allocated obj
delete pd;      //OK
delete pd2;     //undefined: the mem pointed by pd2 was already freed
delete pi2;     //OK: always ok to delete a null ptr

Compiler knows that i is not a ptr. However, compilers cannot tell whether a ptr points to a statically or dynamically allocated obj; and, the compiler cannot tell whether mem addressed by a ptr has already been freed.

Although the value of a const obj cannot be modified, the obj itself can be destroyed.

const int *pci = new const int(1024);
delete pci;     //OK: deletes a const obj

Dynamic memory managed through built-in pointers (rather than smart pointers) exists until it is explicitly freed. After being deleted, the ptr becomes what is referred to as a dangling pointer. We can assign nullptr to the ptr after using delete.

However, resetting the value of a ptr after delete provides only limited protection:

int *p(new int(42));    //p points to dynamic mem
auto q = p;             //p and q point to the same mem
delete p;               //invalidates both p and q
p = nullptr;            //indicates p is no longer bound to an obj
//but, resetting p has no effect on q

(iii) using shared_ptrs with new

If we do not init a smart ptr, it is inited as a null ptr; we can also init a smart ptr from a ptr returned by new:

shared_ptr<double> p1;              //shared ptr that can point to a double
shared_ptr<int> p2(new int(42));    //p2 points to an int with value 42

The smart ptr cstrs that take ptrs are explicit. Hence, we cannot implicitly convert a built-in ptr to a smart ptr; we must use the direct form of initialization to init a smart ptr:

shared_ptr<int> p1 = new int(1024); //ERROR: must use direct init
shared_ptr<int> p2(new int(1024));  //OK: uses direct init

shared_ptr<int> clone(int p) {
    return new int(p);              //ERROR: implicit conversion
}
shared_ptr<int> clone(int p) {
    //OK: explicitly create from int*
    return shared_ptr<int>(new int(p));
}

By default, a ptr used to init a smart ptr must point to dynamic memory because, by default, smart ptrs use delete to free the associated obj.

it is dangerous to use a built-in ptr to access an obj owned by a smart ptr; because we may not know when that obj is destroyed.

//ptr is created and inited when process is called
void process(shared_ptr<int> ptr) {
    //use ptr
}//ptr goes out of scope and is destroyed

shared_ptr<int> p(new int(42)); //ref count is 1
process(p);                     //in process, ref count is 2
int i = *p;                     //OK: ref count is 1

int *x(new int(1024));          //danger: x is a plain ptr
process(x);                     //ERROR: cannot convert int* to shared_ptr<int>
process(shared_ptr<int>(x));    //legal, but the mem will be deleted
int j = *x;                     //undefined: x is a dangling ptr

(iv) other shared_ptr operations

get returns a built-in ptr to the obj that the smart ptr is managing. The func is intended for cases when we need to pass a built-in ptr to code that can't use a smart ptr. The code that uses the return from get must not delete that ptr.

shared_ptr<int> p(new int(42)); //ref count is 1
int *q = p.get();               //OK: but don't use q in any way that might delete its ptr
{ //new block
    //undefined: two independent shared_ptrs point to the same mem
    shared_ptr<int> (q);
} //block ends, q is destroyed, and the mem to which q points is freed
int foo = *p;           //undefined: the mem to which p points was freed

Don't use get to initialize or assign another smart pointer.

reset to assign a new ptr to a shared_ptr, by updating the ref count and, if appropriate, deletes the obj to which p points:

p.reset(new int(1024));         //OK: p points to a new obj

if(!p.unique())
    p.reset(new string(*p));    //we aren't alone, allocate a new copy
*p += newVal;   //now that we know we're the only ptr, ok to change this obj

(v) smart pointers and exceptions

when we use a smart ptr, the smart ptr class ensures that mem is freed when it is no longer needed even if the block is exited permaturely:

void f {
    shared_ptr<int> sp(new int(42));    //allocate a new obj
    //code that throws an exception that is not caught inside f
} //shared_ptr freed auto when the func ends

In contrast, mem that we manage directly is not auto freed when an exception occurs. If we use built-in ptrs to manage mem and an exception occurs after a new but before the crspding delete, then that mem won't be freed:

void f {
    int *ip = nw int(42);   //dynamically allocate a new obj
    //code that throws an exception that is not caught inside f
    delete ip;              //free the mem before exiting
}

Classes that allocate resources - and that don't define dstrs to free those rscs - can be subject to the same kind of errors that arise when we use dynamic memory. It is easy to forget to release the rscs. Similarly, if an exception happens between when the rscs is allocated and when it is freed, the program will leak that rsc.

struct destination;                 //represents what we are connecting to
struct connection;                  //info needed to use the connection

connection connect(destination*);   //open the connection
void disconnect(connection);        //close the given conn

void f(destination &d /* other paras */){
    //get a conn; must remember to close it when done
    connection c = connect(&d);
    //use the connection
    //if we forget to call disconnect, there will be noway to close c
}

If connection had a dstr, that dstr would auto close the connection when f completes. However, no dstr is provided.

We can use shared_ptr to ensure that the connection is properly closed, by using our own deletion code.

void end_connection(connection *p) { disconnect(*p); }

void f(destination &d /* other paras */){
    //get a conn; must remember to close it when done
    connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
    //use the connection
    //if we forget to call disconnect, there will be noway to close c
}

4. unique_ptr ↑top

A unique_ptr "owns" the obj to which it points. Unlike shared_ptr, only one unique_ptr at a time can point to a given obj. The obj to which a unique_ptr points is destroyed when the unique_ptr is destroyed.

unique_ptr<double> p1;              //unique_ptr that can point at a double
unique_ptr<int> p2(new int(42));    //p2 points to int with value 42

Because a unique_ptr owns the obj to which it points, it does not support ordinary copy or assignment:

unique_ptr<string> p1(new string("abcd"));
unique_ptr<string> p2(p1);  //ERROR: no copy for unique_ptr
unique_ptr<string> p3;
p3 = p2;                    //ERROR: no assign for unique_ptr

Although we can't copy or assign unique_ptr, we can transfer ownership from one (nonconst) unique_ptr to another by calling release or reset:

//transfers ownership from p1 (which points to the string) to p2
unique_ptr<string> p2(p1.release());    //release makes p1 null
unique_ptr<string> p3(new string("Trex"));
//transfers ownership from p3 to p2
p2.reset(p3.release());     //reset deletes the mem to which p2 had pointed

The release returns the ptr currently stored in the unique_ptr and makes that unique_ptr null; thus, p2 is inited from the ptr value that had been stored in p1 and p1 becomes null. The reset takes an optional ptr and repositions the unique_ptr to point to the given ptr. If the unique_ptr is not null, then the obj to which the unique_ptr had pointed is deleted. The call to reset on p2, therefore, frees the mem used by the string inited from "abcd", transfers p3's ptr to p2, and makes p3 null.

Calling release breaks the connection bt a unique_ptr and the obj it had been managing. The return is usually used to init or assign another smart ptr; if no smart ptr to hold the return, then the program takes over responsibility for freeing that rsc:

p2.release();           //WRONG: p2 won't free the mem and we've lost the ptr
auto p = p2.release();  //OK, but we must remeber to delete(p)

(i) passing and returning unique_ptrs

There's one exception to the rule that we cannot copy a unique_ptr: we can copy or assign a unique_ptr that is about to be destroyed.

unique_ptr<int> clone(int p) {
    //OK: explicitly create a unique_ptr<int> from int*
    return unique_ptr<int>(new int(p));
}

unique_ptr<int> clone(int p) {
    unique_ptr<int> ret(new int(p));
    // ...
    return ret;
}

In both cases, the compiler knows that the obj being returned is about to be destroyed, and hence, the compiler does a special kind of "copy".

(ii) passing a deleter to unique_ptr

overriding the deleter in a unique_ptr affects the unique_ptr type as well as we construct (or reset) objs of that type.

//p points to an obj of type objT and uses an obj of type delT to free that obj
//it will call an obj named fcn of type delT
unique_ptr<objT, delT> p(new objT, fcn);

void f(destination &d /* other paras */){
    connection c = connect(&d); //open the conn
    //when p is destroyed, the conn will be closed
    unique_ptr<connection, decltype(end_connection)*>
        p(&c, end_connection);
    //use the connection
    //when f exits, even if by an exception, the conn will be properly closed
}

5. weak_ptr ↑top

A weak_ptr is a smart ptr that does not control the lifetime of the obj to which it points. Instead, a weak_ptr points to an obj that is managed by a shared_ptr. Binding a weak_ptr to a shared_ptr does not change the ref count of that shared_ptr. Once the last shared_ptr pointing to the obj goes away, the obj itself will be deleted. The obj will be deleted even if there are weak_ptr pointing to it.

auto p = make_shared<int>(42);
weak_ptr<int> wp(p);    //wp weakly shares with p; use count in p is unchanged

Because the obj might no longer exist, we cannot use a weak_ptr to access its obj directly. To access the obj, we must call lock, which checks whether the obj to which the weak_ptr points still exists. if so, lock returns a shared_ptr to the shared obj.

if(shared_ptr<int> np = wp.lock()) { //true if np is not null
    //inside the if, np shares its obj with p
}

6. Dynamic arrays ↑top

The language and library provide two ways to allocate an array of objects at once. The language defines a second kind of new expr that allocates and initializes an array of objs; the library includs a template class named allocator that lets us separate allocation from initialization.

new and arrays

new allocates the requested number of objs and returns a ptr to the first one, instead getting an obj of array type:

//call get_size to determine how many ints to allocate
int *pia = new int[get_size()];     //pia points to the 1st of these ints

//can also allocate using a type alias
typedef int arrT[42];               //arrT names the type arr of 42 ints
int *p = new arrT;                  //allocates an arr of 42 ints
//compiler executes as if we had written int *p = new int[42];

initializing an array of dynamically allocated objects:

int *pia = new int[10];             //block of ten uninited ints
int *pia2 = new int[10];            //block of ten ints value inited to 0
string *psa = new string[10];       //block of ten empty strings
string *psa2 = new string[10]();    //ditto

//block of ten ints each inited from the crspding initializer
int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
//block of ten strings; first four are inited from the given initers
//remaining elements are value inited
string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};

freeing dynamic arrays
Elements in an array are destroyed in reverse order, i.e., from last to first.

delete p;                           //p must point to a dynamically allocated obj or be null
delete [] pa;                       //pa must point to dynamically allocated arr or be null

smart pointers and dynamic arrays
The library provides a version of unique_ptr that can manage arrays allocated by new.

//up points to an arr of ten uninited ints
unique_ptr<int[]> up(new int[10]);
up.release;                         //auto uses delete[] to destroy its ptr

for(size_t i=0; i != 10; ++i)
    up[i] = i;                      //assign a new value to each of the elems

unlike unique_ptr, shared_ptrs provide no direct support for managing a dynamic array, and we must provide our own deleter to manage:

//to use a shared_ptr we must supply deleter
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
sp.reset();                         //uses the lambda to free the array

//shared_ptrs don't have subscript operator and don't support ptr arithmetic
for(size_t i=0; i != 10; ++i)
    *(sp.get() + i) = i;            //use get to get a built-in ptr

7. Allocator ↑top

In general, coupling allocation and construction can be wasteful:

string *const p = new string[n];    //construct n empty strings
string s;
string *q = p;                      //q points to the first string
while(cin >> s && q != p+n)
    *q++ = s;                       //assign a new value to *q
const size_t size = q - p;          //remember how many strings we read
//use the array
delete[] p;                         //p points to an arr; must use delete[]

The new expr allocates and inits n strings, which might be larger than what we need. Moreover, for those objs we do use, we immediately assign new values over the previously inited strings. The elems that are used are written twice: first when the elems are default inited; and subsequently when we assign to them. More importantly, classes that do not have default cstrs cannot be dynamically allocated as array.

allocator class

the library allocator class lets us separate allocation from construction. It provides type-aware allocation of raw, unconstructed, memory.

When an allocator obj allocates memory, it allocates mem that is appropriately sized and aligned to hold objs of the given type:

allocator<string> alloc;            //obj that can allocate strings
auto const p = alloc.allocate(n);   //allocate n unconstructed strings

The mem an allocator allocates is unconstructed. We must construct objects in order to use mem returned by allocate:

auto q = p;                         //q will point to one past the last csted elem
alloc.construct(q++);               //*q is the empty str
alloc.construct(q++, 10, 'c');      //*q is cccccccccc
alloc.construct(q++, "hi");         //*q is hi!

cout << *p << endl;                 //OK: uses the string output operator
cout << *q << endl;                 //disaster: q points to unconstructed mem

when we're finished using the objs, we must destroy the elems we constructed, by calling destroy on each constructed elem:

while (q != p)
    alloc.destroy(--q);             //free the strings we actually allocated

once the elems have been destroyed, we can either reuse the mem to hold other strings or return the mem to the system. we free the mem by calling deallocate:

alloc.deallocate(p, n);

algorithms to copy and fill uninitialized memory
As a companion to the allocator class, the library also defines two algs that can construct objs in uninited mem.

Assume we have a vector of ints that we want to copy into dynamic mem. We'll allocate mem for twice as many ints as are in the vector. We'll construct the first half of the newly allocated mem by copying elems from the orig vector, and construct elems in the second half by filling them with a given value:

//allocate twice as many elems as vi holds
auto p = alloc.allocate(vi.state() * 2);
//construct elems starting at p as copies of elems in vi
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
//init the remaining elems to 42
uninitalized_fill_n(q, vi.size(), 42);