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
- Pointer that knows when it dangles
- Useful
- Cyclic references
- Pinters to a resource with decouple lifecycle
- Observer but not owner
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.
- No, changes in
// 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()