C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
48: Smart Pointers: shared_ptr
A few years back, I was helping a junior dev on a game engine project. He had created a Texture class and was using std::unique_ptr to manage it within a Level class. That seemed fine until he added Sprite objects that all needed to reference that same texture. He started passing raw pointers from the unique_ptr to the sprites. It worked—until the Level was destroyed before the Sprites were. The next time a sprite tried to draw, the program crashed with a segmentation fault because it was staring at a piece of memory that had already been reclaimed. He spent an entire weekend trying to manually track the "lifespan" of the texture, which is a losing battle in any complex system.
This is exactly why std::shared_ptr exists. While unique_ptr is about exclusive ownership, shared_ptr is about shared ownership. It allows multiple parts of your code to hold a pointer to the same resource, and the resource is only destroyed when the very last owner lets go. I like to think of it as a group chat: the chat exists as long as at least one person is still in it. Once the last person leaves, the chat is deleted.
The Mechanics of Reference Counting
Under the hood, std::shared_ptr doesn't just store the raw pointer. It manages a "control block" that keeps a reference count. Every time you copy a shared_ptr, the count increments. When a shared_ptr goes out of scope or is reset, the count decrements. When that count hits zero, the destructor of the managed object is called, and the memory is freed.
#include <iostream>
#include <memory>
#include <vector>
struct GameAsset {
std::string name;
GameAsset(std::string n) : name(n) { std::cout < name > loaded\n"; }
~GameAsset() { std::cout < name > destroyed\n"; }
};
void useAsset(std::shared_ptr<GameAsset> asset) {
std::cout < "Using " < asset->name < " inside a function\n";
// Reference count is incremented here because we passed by value
}
int main() {
// Always use std::make_shared instead of 'new' for better performance
auto texture = std::make_shared<GameAsset>("BrickWall_Diffuse");
{
std::shared_ptr<GameAsset> anotherReference = texture;
std::cout < "Reference count: " < texture.use_count() < "\n"; // Outputs 2
}
std::cout < "Reference count: " < texture.use_count() < "\n"; // Back to 1
useAsset(texture);
return 0;
}
You'll notice I used std::make_shared. Please, do yourself a favor and make this a habit. It performs a single memory allocation for both the object and the control block, whereas using std::shared_ptr<T>(new T()) does two separate allocations. It's faster and more exception-safe.
The Trap of Circular References
Now, shared_ptr is powerful, but it has one glaring weakness that has bitten me more times than I care to admit: the circular dependency. Imagine you have a Parent object that holds a shared_ptr to a Child, and that Child holds a shared_ptr back to the Parent.
In this scenario, neither object will ever be deleted. The parent's reference count will never hit zero because the child holds a pointer to it, and the child's count will never hit zero because the parent holds a pointer to it. You've just created a memory leak that the "smart" pointer can't solve. In the real world, this often happens with observers or back-pointers in tree structures. To break this cycle, we use std::weak_ptr, which allows you to reference an object without increasing its reference count. We'll dive deeper into that in the next lesson, but for now, just keep in mind that shared ownership should be used judiciously—don't just use it because you're too lazy to figure out who actually "owns" the object.
📋 Practical Task
Exercise: Building a Shared Configuration Registry
Imagine you are building a server application where multiple different modules (e.g., DatabaseModule, NetworkModule, and LoggingModule) all need access to a single AppConfig object. If the configuration is updated or the system shuts down, you want to ensure the memory is managed correctly without any module holding a dangling pointer.
Your Task:
- Create a class
AppConfigthat contains astd::string serverNameand aint port. Add a constructor and a destructor that prints a message when the config is created and destroyed. - Create three different classes:
DatabaseModule,NetworkModule, andLoggingModule. Each of these classes should have a constructor that accepts astd::shared_ptr<AppConfig>and stores it as a member variable. - In your
mainfunction:- Create a single
AppConfiginstance usingstd::make_shared. - Instantiate the three modules, passing the shared config to each.
- Print the
use_count()of the config pointer at various stages (after creation, after module instantiation, and after one module is destroyed). - Ensure that the
AppConfigis only destroyed after all modules have gone out of scope.
- Create a single
There are no comments for now.