Modern C++: Smart Pointers - CYF Blog

Modern C++: Smart Pointers

Polymorphism and heap memory

Polymorphism with Inheritance

Before learning smart pointer, we need some review for polymorphism and heap memory. The most important feature of Polymorphism is heterogenous collection

class GeoObj {
public:
    GeoObj() = default;
    virtual void draw() const = 0;
    virtual ~GeoObj() = default;
    ...
};

class Circle : public GeoObj {
private:
    Coord center;
    int rad;
public:
    Circle(Coord c, int r);
    virtual void draw() const override;
    ...
};

class Line : public GeoObj {
private:
    Coord from;
    Coord to;
public:
    Line(Coord f, Coord t);
    virtual void draw() const override;
    ...
}
std::vector<GeoObj*> p;    // heterogenous collection
Line l{Coord{1,2}, Coord{3, 4}};
Circle c{Coord{5,5}, 7};
p.push(&l);
p.push(&c);

for(GeoObj* gp : p) {
    p->draw();    // polymorphic call
}

std::vector<GeoObj*> createPicture() {
    std::vector<GeoObj*> p;    // heterogenous collection
    Line l{Coord{1,2}, Coord{3, 4}};
    Circle c{Coord{5,5}, 7};
    p.push(&l);
    p.push(&c);
    return p;    // Fatal runtime error: Returns vector with destroyed pointers
}

std::vector<GeoObj*> pict = creaturePicture();

for(GeoObj* gp: pict) {
    gp->draw();    //ERROR(undefined behavior)
}

Polymorphism with Heap Memory

To keep the value without destorying in stack memory, we create the memory in heap

std::vector<GeoObj*> createPicture() {
    std::vector<GeoObj*> p;    // heterogenous collection
    Line* lp = new Line{Coord{1,2}, Coord{3, 4}};
    Circle* cp = new Circle{Coord{5,5}, 7};
    p.push(lp);
    p.push(cp);
    return p;
}

std::vector<GeoObj*> pict = creaturePicture();

for(GeoObj* gp: pict) {
    gp->draw();    //ERROR(undefined behavior)
}

// remove all element without memory leak:
for(GeoObj*& geoPtr: pict) { // reference to pointer as we assign a new value to each element
    delete geoPtr;
    geoPtr = nullptr; // in case we don't remove the element
}

pict.clear();

Can we have something clean up for us?

Smart Pointers

  • Smart pointers
    • Object can be used like pointers, but are smarter
    • Act as owners of the objects
      • Call delete for the objects they point to when they are is no longer used (owned)
  • Shared pointers
    • Shared ownership
    • Some overhead
  • Unique pointers
    • Exlusive ownership
    • No overhead

Shared Pointers

// define type alias:
using GeoPtr = std::shared_ptr<GeoObj>;

std::vector<GeoPtr> creaturePicture() {
    std::vector<GeoPtr> p;
    auto lp = std::make_shared<Line>(Coord{1, 2}, Coord{3, 4});
    // std::shared_ptr<Line> lp{new Line{Coord{1,2}, Coord{3, 4}}};
    auto cp = std::make_shared<Circle>(Coord{5, 5}, 7);
    // std::shared_ptr<Circle> cp{new Circle{Coord{1,2}, 7}};
    p.push(lp);
    p.push(cp);
    return p;
}

std::vector<GeoPtr> pict = creaturePicture();

During the function call createPicture()

After the end of call createPicture() After the call pict.clear()

Weak Pointers

  • Class weak_ptr
    • Observer but not owner
      • Pointer that knows when it dangles
        • Allows code to share but not own an object or resource
    • Useful
      • Cyclic references
      • Pinters to a resource with decouple lifecycle

If we delete the shared_ptr points to resource A, we could still ask the weak_ptr “Is this still an object”, which you cannot do in raw pointer. Raw pointers point the memory which may become meantime some other object, but weak pointers know when it is dangled.

Weak pointers might hold memory after deleter is called

{
    std::weak_ptr<R> wp;
    {
        std::shared_ptr<R> gp;
        {
            auto sp = std::make_shared<R>();
            gp = sp;
            wp = sp;
        } // (1)
    }     // (2)
    ...
}         // (3) 

(1) (2) (3)

  • The control block is bounded by the resource R, which means even though you thought the object is destoryed, but the entire object memory is still there until the weaks counter counts to 0.
  • Simple walk around solution: use std::shared_ptr<R> sp{new R};. Two step initialization will seperate the memory and control block, which make control block memory and resource seperately.

How is the performance of shared pointers?

  • Does copying shared pointers in different threads cause a data race?
    • No, changes in use_count() do not reflect modifications that can introduce data races.
// initalize vector with 1000 shared pointers:
std::vector<std::shared_ptr<T>> coll;
for(int i = 0; i < 1000; ++i) {
    coll.push_back(std::make_shared<T>());
}

int numIterations = 1'000'000;

void threadLoop(int numThreads) 
{
    // loop 1 milllion times (partitioned over all thread) overall all pointers:
    // & optional => optional copying the shared pointers
    for(auto& sp: coll) {            // By reference can be faster by a factor of 2 to 1000
        sp->incrementLocalInt();
    }
}

In conclusion, pass shared and weak pointers by reference

Unique Pointers

// define type alias:
using GeoPtr = std::unique_ptr<GeoObj>;

std::vector<GeoPtr> creaturePicture() {
    std::vector<GeoPtr> p;
    auto lp = std::make_unique<Line>(Coord{1, 2}, Coord{3, 4});
    // std::unique_ptr<Line> lp{new Line{Coord{1,2}, Coord{3, 4}}};
    auto cp = std::make_unique<Circle>(Coord{5, 5}, 7);
    // std::unique_ptr<Circle> cp{new Circle{Coord{1,2}, 7}};
    
    p.push(lp); // ERROR: copying diabled
    p.push(cp); // ERROR: copying diabled
    
    p.push(std::move(lp)); // moves ownership
    p.push(std::move(cp)); // moves ownership
     
    return p;
}

Unique Pointers And Polymorphism

  • Downcasts do not work for unique pointers
    • They will create a second owner to the object
    • You have to use temporary raw pointer
class GeoObj {...};
class Circle : public GeoObj {...};

std::vector<std::unique_ptr<GeoObj>> geoObjs;
geoObjs.push_back(std::make_unique<Circle>(...));  // OK, insert circle into collection

const auto& p = geoObjs[0];                         // p is unique_ptr<GeoObj>
std::unique_ptr<Circle> cp{p};                      // comiple-time Error
auto cp(dynamic_cast<std::unique_ptr<Circle>>(p));  // compile-time Error
auto cp = dynamic_cast<std::unique_ptr<Circle>>(p); // compile-time Error

if(auto cp = dynamic_cast<Circle*>(p.get())) {      // OK (temporary raw pointer)
    ... //use cp as Circle*
}

std::variant

Polymorphism with std::variant<>

using GeoObjVar = std::variant<Circle, Line>;

std::vecotr<GeoObjVar> createPicture() {
    std::vector<GeoObjVar> p;
    p.push_back(Line{Coord{1, 2}, Coord{3, 4}});
    p.push_back(Circle{Coord{5, 5}, 7});
    return p;
}

std::vector<GeoObjVar> pict = creatPicture();
for(const GeoObjVar& geoobj: pict) {
    switch(geoobj.index()) {
        case 0:
            std::get<0>(geoobj).draw();
            break;
        case 1:
            std::get<1>(geoobj).draw();
            break;
    }
    
    // or using visit
    std::visit([] (const auto& obj) {
        obj.draw(); //polymorphic call
    },
    geoobj);
}

pict.clear()