COMS W4995 C++ for C Programmers

Index of 2025-1/code/13

Parent directory
Makefile
shared_ptr.cpp
smartptr.cpp
smartptr.h
unique_ptr.cpp

Makefile

CC  = g++
CXX = g++

CFLAGS   = -g -Wall
CXXFLAGS = -g -Wall -std=c++17

# turn off warnings about unused variables
CXXFLAGS += -Wno-unused-variable

executables = smartptr shared_ptr unique_ptr

.PHONY: default
default: $(executables)

smartptr: smartptr.o

shared_ptr: shared_ptr.o

unique_ptr: unique_ptr.o

smartptr.o: smartptr.h

shared_ptr.o: smartptr.h

.PHONY: clean
clean:
	rm -f *~ a.out core *.o $(executables)

.PHONY: all
all: clean default

shared_ptr.cpp

#include <string>
#include <vector>
#include <iostream>
#include <iomanip>
#include <utility>
#include <memory>
#include <cassert>
#include "smartptr.h"
using namespace std;

int main() {
    string*            p0 { new string(50, 'A') };
    shared_ptr<string> p1 { new string(50, 'B') };
    shared_ptr<string> p2 { make_shared<string>(50, 'C') };

    cout << *p0 << '\n' << *p1 << '\n' << *p2 << '\n';
}

/*  Lecture outline:

1.  make_shared<T>() combines allocation of T and the control block of
    the shared_ptr<T>

    - Compare the number of heap allocations using valgrind
    - Draw diagram to illustrate what make_shared<T>() does

2.  shared_ptr to an array and custom deleter

    - Can SmartPtr point to an array?

        SmartPtr<string> p { new string[2] {"hello", "c2cpp"} };
        cout << *p << ' ' << p.getPtr()[1] << '\n';

    - What about shared_ptr?

        shared_ptr<string> p { new string[2] {"hello", "c2cpp"} };
        cout << *p << ' ' << p.get()[1] << '\n';

    - We can pass a custom deleter to shared_ptr:

        shared_ptr<string> p { new string[2] {"hello", "c2cpp"},
            [] (string* s) { delete[] s; }
        };
        cout << *p << ' ' << p.get()[1] << '\n';

    - Or we can specify T as an array type:

        shared_ptr<string[]> p { new string[2] {"hello", "c2cpp"} };
        cout << p[0] << " " << p[1] << '\n';

      Note that operator[] is now available for shared_ptr

3.  Aliasing

        shared_ptr<pair<string,int>> p1 { new pair{"hello"s, 4157} };
        shared_ptr<int>              p2 { p1, &p1->second };

        cout << p1->first << ' ' << *p2 << '\n';

        cout << p1.use_count() << '\n';
        cout << p2.use_count() << '\n';

4.  weak_ptr can be attached to shared_ptr without increasing ref count

        auto weak_test = [](weak_ptr<string> wp) {
            if (shared_ptr<string> sp = wp.lock()) {
                cout << *sp << '\n';
            } else {
                cout << "weak_ptr expired\n";
            }
        };

        weak_ptr<string> wp;

        {
            auto sp = make_shared<string>("hello");
            assert(sp.use_count()==1);

            wp = sp;                   // weak_ptr does not increase
            assert(sp.use_count()==1); // the shared_ptr's ref count

            weak_test(wp); // sp is alive, so is wp
        }

        weak_test(wp); // sp is gone, so wp is expired
*/

smartptr.cpp

#include <string>
#include <vector>
#include <iostream>
#include <iomanip>
#include <utility>
#include <memory>
#include "smartptr.h"
using namespace std;

auto create_vec() {

    // SmartPtr<T> replaces T* for heap-allocated object.
    // That is, instead of doing
    //
    //     string *p = new string("hello");
    //
    // We do the following:

    SmartPtr<string> p1 { new string("hello") };
    SmartPtr<string> p2 { new string("c") };
    SmartPtr<string> p3 { new string("students") };

    vector v { p1, p2, p3 };
    return v;
}

int main() {
    vector v1 = create_vec();
    for (auto&& p : v1) { cout << *p + " "; } cout << '\n';

    vector v2 = v1;
    *v2[1] = "c2cpp";
    *v2[2] = "hackers"; 

    // print v1 again
    for (auto&& p : v1) { cout << *p + " "; } cout << '\n';
}

/*  Lecture outline:

1.  Using SmartPtr<T> instead of T*

    - create_vec() and main() code walk-thru
    - SmartPtr<T> acts like T*, and no delete needed at the end

2.  (Digression) A couple of new things about auto

    - Letting compiler deduce return type using auto

    - Using auto&& in range-for loop
        - auto&& is a forwarding reference just like function template's T&&

    - Four different ways to use auto in a range-for loop:

        1) for (auto x : v) -- x is a copy of each element of v
        2) for (auto& x : v) -- x is an l-value-ref to each element
        3) for (auto&& x : v) -- x is either an l-value-ref or r-value-ref,
                                 depending on what '*v.begin()' is.
        4) for (const auto& x : v) -- x is a const l-value-ref

        Note on 3): vector<bool> is an example where you get a temporary
        object (i.e., an rvalue) when dereferencing an iterator.

3.  SmartPtr implementation

    - Drawing of SmartPtr basic-4 operations
    - SmartPtr code walk-thru
    - Drawing of create_vec() and main()

4.  MakeSmartPtr

    template <typename T, typename... Args>
    SmartPtr<T> MakeSmartPtr(Args&&... args) {
        return SmartPtr<T>(new T(std::forward<Args>(args)...));
    }

    auto create_vec() {
        auto p1 { MakeSmartPtr<string>("hello") };
        auto p2 { MakeSmartPtr<string>("c") };
        auto p3 { MakeSmartPtr<string>("students") };

        vector v { p1, p2, p3 };
        return v;
    }

5.  Real smart pointer in STL: shared_ptr<T> and make_shared<T>

    auto create_vec() {
        // make_shared<T>() function template creates shared_ptr<T>

        auto p1 { make_shared<string>("hello") };
        auto p2 { make_shared<string>("c") };
        auto p3 { make_shared<string>("students") };

        vector v { p1, p2, p3 };
        return v;
    }

6.  (Digression) static_assert, decltype, is_same_v

    static_assert(is_same_v<decltype(p3), shared_ptr<string>>);
*/

smartptr.h

template <class T>
class SmartPtr {
    T*   ptr;    // the underlying pointer
    int* count;  // the reference count
public:
    explicit SmartPtr(T *p = 0) : ptr{p}, count{new int(1)}  {}
    ~SmartPtr() {
        if (--*count == 0) {
            delete count;
            delete ptr;
        }
    }

    SmartPtr(const SmartPtr<T>& sp) : ptr(sp.ptr), count(sp.count) {
        ++*count; 
    }
    SmartPtr<T>& operator=(const SmartPtr<T>& sp) {
        if (this != &sp) {
            // first, detach.
            if (--*count == 0) {
                delete count;
                delete ptr;
            }
            // attach to the new object.
            ptr = sp.ptr;
            count = sp.count;
            ++*count;
        }
        return *this;
    }

    T& operator*() const { return *ptr; }
    T* operator->() const { return ptr; }

    T* getPtr() const { return ptr; } // access to underlying ptr
    operator void*() const { return ptr; } // enables "if (sp) ..."
};

unique_ptr.cpp

#include <string>
#include <vector>
#include <iostream>
#include <iomanip>
#include <utility>
#include <memory>
#include <cassert>
using namespace std;

unique_ptr<string[]> create_array(size_t n, string s) {
    auto up = make_unique<string[]>(n);
    for (size_t i = 0; i < n; ++i) {
        up[i] = s;
    }
    return up;
}

int main() {
    size_t n = 1000;
    unique_ptr<string[]> up1 = create_array(n, "hello");

    for (size_t i = 0; i < n; ++i) { assert(up1[i] == "hello"s); }

    unique_ptr<string[]> up2;
    assert(!up2);

    // This doesn't compile -- unique_ptr cannot be copied
    // up2 = up1;

    // But it can be moved
    up2 = std::move(up1);
    assert(!up1 && up2);

    // It can also be moved to a shared_ptr
    shared_ptr<string[]> sp = std::move(up2);
    assert(!up2 && sp.use_count() == 1);

    for (size_t i = 0; i < n; ++i) { assert(sp[i] == "hello"s); }
}

/*  Lecture outline:

1.  std::unique_ptr is a simpler smart pointer

    - It deletes the underlying object when it gets destroyed, just like
      shared_ptr, but it doesn't do any reference counting.

    - Thus, unique_ptr needs to ensure that there is only one unique_ptr
      pointing to the underlying object at any time. It achieves this by
      disallowing copy -- i.e., it cannot be copied; it can only be moved.

    - This sample program demonstrates how it works.

    - In most situations where you need a smart poninter, you don't really
      need more than one pointer at a time. Start with a unique_ptr and 
      switch to shared_ptr only when needed.
*/